/*! p5.js v2.2.1 February 11, 2026 */
var p5 = (function () {
  'use strict';

  /**
   * @module Constants
   * @submodule Constants
   * @for p5
   */

  const _PI = Math.PI;

  /**
   * Version of this p5.js.
   * @property {String} VERSION
   * @final
   */
  const VERSION = '2.2.1';

  // GRAPHICS RENDERER
  /**
   * The default, two-dimensional renderer in p5.js.
   *
   * Use this when calling <a href="#/p5/createCanvas"> (for example,
   * `createCanvas(400, 400, P2D)`) to specify a 2D context.
   *
   * @typedef {'p2d'} P2D
   * @property {P2D} P2D
   * @final
   */
  const P2D = 'p2d';

  /**
   * A high-dynamic-range (HDR) variant of the default, two-dimensional renderer.
   *
   * When available, this mode can allow for extended color ranges and more
   * dynamic color representation. Use it similarly to `P2D`:
   * `createCanvas(400, 400, P2DHDR)`.
   *
   * @typedef {'p2d-hdr'} P2DHDR
   * @property {P2DHDR} P2DHDR
   * @final
   */

  const P2DHDR = 'p2d-hdr';

  /**
   * One of the two render modes in p5.js, used for computationally intensive tasks like 3D rendering and shaders.
   *
   * `WEBGL` differs from the default <a href="/reference/p5/P2D">`P2D`</a> renderer in the following ways:
   *
   * - **Coordinate System** - When drawing in `WEBGL` mode, the origin point (0,0,0) is located at the center of the screen, not the top-left corner. See <a href="https://p5js.org/tutorials/coordinates-and-transformations/">the tutorial page about coordinates and transformations</a>.
   * - **3D Shapes** - `WEBGL` mode can be used to draw 3-dimensional shapes like <a href="#/p5/box">box()</a>, <a href="#/p5/sphere">sphere()</a>, <a href="#/p5/cone">cone()</a>, and <a href="https://p5js.org/reference/#3D%20Primitives">more</a>. See <a href="https://p5js.org/tutorials/custom-geometry/">the tutorial page about custom geometry</a> to make more complex objects.
   * - **Shape Detail** - When drawing in `WEBGL` mode, you can specify how smooth curves should be drawn by using a `detail` parameter. See <a href="https://github.com/processing/p5.js/wiki/Getting-started-with-WebGL-in-p5#3d-primitives-shapes">the wiki section about shapes</a> for a more information and an example.
   * - **Textures** - A texture is like a skin that wraps onto a shape. See <a href="https://github.com/processing/p5.js/wiki/Getting-started-with-WebGL-in-p5#textures">the wiki section about textures</a> for examples of mapping images onto surfaces with textures.
   * - **Materials and Lighting** - `WEBGL` offers different types of lights like <a href="#/p5/ambientLight">ambientLight()</a> to place around a scene. Materials like <a href="#/p5/specularMaterial">specularMaterial()</a> reflect the lighting to convey shape and depth. See <a href="https://p5js.org/tutorials/lights-camera-materials/">the tutorial page for styling and appearance</a> to experiment with different combinations.
   * - **Camera** - The viewport of a `WEBGL` sketch can be adjusted by changing camera attributes. See <a href="https://p5js.org/tutorials/lights-camera-materials#camera-and-view">the tutorial page section about cameras</a> for an explanation of camera controls.
   * - **Text** - `WEBGL` requires opentype/truetype font files to be preloaded using <a href="#/p5/loadFont">loadFont()</a>. See <a href="https://github.com/processing/p5.js/wiki/Getting-started-with-WebGL-in-p5#text">the wiki section about text</a> for details, along with a workaround.
   * - **Shaders** - Shaders are hardware accelerated programs that can be used for a variety of effects and graphics. See the <a href="https://p5js.org/tutorials/intro-to-shaders/">introduction to shaders</a> to get started with shaders in p5.js.
   * - **Graphics Acceleration** - `WEBGL` mode uses the graphics card instead of the CPU, so it may help boost the performance of your sketch (example: drawing more shapes on the screen at once).
   *
   * To learn more about WEBGL mode, check out <a href="https://p5js.org/tutorials/#webgl">all the interactive WEBGL tutorials</a> in the "Tutorials" section of this website, or read the wiki article <a href="https://github.com/processing/p5.js/wiki/Getting-started-with-WebGL-in-p5">"Getting started with WebGL in p5"</a>.
   *
   * @typedef {'webgl'} WEBGL
   * @property {WEBGL} WEBGL
   * @final
   */
  const WEBGL = 'webgl';
  /**
   * One of the two possible values of a WebGL canvas (either WEBGL or WEBGL2),
   * which can be used to determine what capabilities the rendering environment
   * has.
   * @typedef {'webgl2'} WEBGL2
   * @property {WEBGL2} WEBGL2
   * @final
   */
  const WEBGL2 = 'webgl2';

  /**
   * A constant used for creating a WebGPU rendering context
   * @property {'webgpu'} WEBGPU
   * @final
   */
  const WEBGPU = 'webgpu';

  // ENVIRONMENT
  /**
   * @typedef {'default'} ARROW
   * @property {ARROW} ARROW
   * @final
   */
  const ARROW = 'default';

  /**
   * @property {String} SIMPLE
   * @final
   */
  const SIMPLE = 'simple';
  /**
   * @property {String} FULL
   * @final
   */
  const FULL = 'full';

  /**
   * @typedef {'crosshair'} CROSS
   * @property {CROSS} CROSS
   * @final
   */
  const CROSS = 'crosshair';
  /**
   * @typedef {'pointer'} HAND
   * @property {HAND} HAND
   * @final
   */
  const HAND = 'pointer';
  /**
   * @typedef {'move'} MOVE
   * @property {MOVE} MOVE
   * @final
   */
  const MOVE = 'move';
  /**
   * @typedef {'text'} TEXT
   * @property {TEXT} TEXT
   * @final
   */
  const TEXT = 'text';
  /**
   * @typedef {'wait'} WAIT
   * @property {WAIT} WAIT
   * @final
   */
  const WAIT = 'wait';

  // TRIGONOMETRY

  /**
   * A `Number` constant that's approximately 1.5708.
   *
   * `HALF_PI` is half the value of the mathematical constant π. It's useful for
   * many tasks that involve rotation and oscillation. For example, calling
   * `rotate(HALF_PI)` rotates the coordinate system `HALF_PI` radians, which is
   * a quarter turn (90˚).
   *
   * Note: `TWO_PI` radians equals 360˚, `PI` radians equals 180˚, `HALF_PI`
   * radians equals 90˚, and `QUARTER_PI` radians equals 45˚.
   *
   * @property {Number} HALF_PI
   * @final
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Draw an arc from 0 to HALF_PI.
   *   arc(50, 50, 80, 80, 0, HALF_PI);
   *
   *   describe('The bottom-right quarter of a circle drawn in white on a gray background.');
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Translate the origin to the center.
   *   translate(50, 50);
   *
   *   // Draw a line.
   *   line(0, 0, 40, 0);
   *
   *   // Rotate a quarter turn.
   *   rotate(HALF_PI);
   *
   *   // Draw the same line, rotated.
   *   line(0, 0, 40, 0);
   *
   *   describe('Two black lines on a gray background. One line extends from the center to the right. The other line extends from the center to the bottom.');
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   describe(
   *     'A red circle and a blue circle oscillate from left to right on a gray background. The red circle appears to chase the blue circle.'
   *   );
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Translate the origin to the center.
   *   translate(50, 50);
   *
   *   // Calculate the x-coordinates.
   *   let x1 = 40 * sin(frameCount * 0.05);
   *   let x2 = 40 * sin(frameCount * 0.05 + HALF_PI);
   *
   *   // Style the oscillators.
   *   noStroke();
   *
   *   // Draw the red oscillator.
   *   fill(255, 0, 0);
   *   circle(x1, 0, 20);
   *
   *   // Draw the blue oscillator.
   *   fill(0, 0, 255);
   *   circle(x2, 0, 20);
   * }
   */
  const HALF_PI = _PI / 2;

  /**
   * A `Number` constant that's approximately 3.1416.
   *
   * `PI` is the mathematical constant π. It's useful for many tasks that
   * involve rotation and oscillation. For example, calling `rotate(PI)` rotates
   * the coordinate system `PI` radians, which is a half turn (180˚).
   *
   * Note: `TWO_PI` radians equals 360˚, `PI` radians equals 180˚, `HALF_PI`
   * radians equals 90˚, and `QUARTER_PI` radians equals 45˚.
   *
   * @property {Number} PI
   * @final
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Draw an arc from 0 to PI.
   *   arc(50, 50, 80, 80, 0, PI);
   *
   *   describe('The bottom half of a circle drawn in white on a gray background.');
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Translate the origin to the center.
   *   translate(50, 50);
   *
   *   // Draw a line.
   *   line(0, 0, 40, 0);
   *
   *   // Rotate a half turn.
   *   rotate(PI);
   *
   *   // Draw the same line, rotated.
   *   line(0, 0, 40, 0);
   *
   *   describe('A horizontal black line on a gray background.');
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   describe(
   *     'A red circle and a blue circle oscillate from left to right on a gray background. The circles drift apart, then meet in the middle, over and over again.'
   *   );
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Translate the origin to the center.
   *   translate(50, 50);
   *
   *   // Calculate the x-coordinates.
   *   let x1 = 40 * sin(frameCount * 0.05);
   *   let x2 = 40 * sin(frameCount * 0.05 + PI);
   *
   *   // Style the oscillators.
   *   noStroke();
   *
   *   // Draw the red oscillator.
   *   fill(255, 0, 0);
   *   circle(x1, 0, 20);
   *
   *   // Draw the blue oscillator.
   *   fill(0, 0, 255);
   *   circle(x2, 0, 20);
   * }
   */
  const PI = _PI;

  /**
   * A `Number` constant that's approximately 0.7854.
   *
   * `QUARTER_PI` is one-fourth the value of the mathematical constant π. It's
   * useful for many tasks that involve rotation and oscillation. For example,
   * calling `rotate(QUARTER_PI)` rotates the coordinate system `QUARTER_PI`
   * radians, which is an eighth of a turn (45˚).
   *
   * Note: `TWO_PI` radians equals 360˚, `PI` radians equals 180˚, `HALF_PI`
   * radians equals 90˚, and `QUARTER_PI` radians equals 45˚.
   *
   * @property {Number} QUARTER_PI
   * @final
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Draw an arc from 0 to QUARTER_PI.
   *   arc(50, 50, 80, 80, 0, QUARTER_PI);
   *
   *   describe('A one-eighth slice of a circle drawn in white on a gray background.');
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Translate the origin to the center.
   *   translate(50, 50);
   *
   *   // Draw a line.
   *   line(0, 0, 40, 0);
   *
   *   // Rotate an eighth turn.
   *   rotate(QUARTER_PI);
   *
   *   // Draw the same line, rotated.
   *   line(0, 0, 40, 0);
   *
   *   describe('Two black lines that form a "V" opening towards the bottom-right corner of a gray square.');
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   describe(
   *     'A red circle and a blue circle oscillate from left to right on a gray background. The red circle appears to chase the blue circle.'
   *   );
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Translate the origin to the center.
   *   translate(50, 50);
   *
   *   // Calculate the x-coordinates.
   *   let x1 = 40 * sin(frameCount * 0.05);
   *   let x2 = 40 * sin(frameCount * 0.05 + QUARTER_PI);
   *
   *   // Style the oscillators.
   *   noStroke();
   *
   *   // Draw the red oscillator.
   *   fill(255, 0, 0);
   *   circle(x1, 0, 20);
   *
   *   // Draw the blue oscillator.
   *   fill(0, 0, 255);
   *   circle(x2, 0, 20);
   * }
   */
  const QUARTER_PI = _PI / 4;

  /**
   * A `Number` constant that's approximately 6.2382.
   *
   * `TAU` is twice the value of the mathematical constant π. It's useful for
   * many tasks that involve rotation and oscillation. For example, calling
   * `rotate(TAU)` rotates the coordinate system `TAU` radians, which is one
   * full turn (360˚). `TAU` and `TWO_PI` are equal.
   *
   * Note: `TAU` radians equals 360˚, `PI` radians equals 180˚, `HALF_PI`
   * radians equals 90˚, and `QUARTER_PI` radians equals 45˚.
   *
   * @property {Number} TAU
   * @final
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Draw an arc from 0 to TAU.
   *   arc(50, 50, 80, 80, 0, TAU);
   *
   *   describe('A white circle drawn on a gray background.');
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Translate the origin to the center.
   *   translate(50, 50);
   *
   *   // Draw a line.
   *   line(0, 0, 40, 0);
   *
   *   // Rotate a full turn.
   *   rotate(TAU);
   *
   *   // Style the second line.
   *   strokeWeight(5);
   *
   *   // Draw the same line, shorter and rotated.
   *   line(0, 0, 20, 0);
   *
   *   describe(
   *     'Two horizontal black lines on a gray background. A thick line extends from the center toward the right. A thin line extends from the end of the thick line.'
   *   );
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   describe(
   *     'A red circle with a blue center oscillates from left to right on a gray background.'
   *   );
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Translate the origin to the center.
   *   translate(50, 50);
   *
   *   // Calculate the x-coordinates.
   *   let x1 = 40 * sin(frameCount * 0.05);
   *   let x2 = 40 * sin(frameCount * 0.05 + TAU);
   *
   *   // Style the oscillators.
   *   noStroke();
   *
   *   // Draw the red oscillator.
   *   fill(255, 0, 0);
   *   circle(x1, 0, 20);
   *
   *   // Draw the blue oscillator, smaller.
   *   fill(0, 0, 255);
   *   circle(x2, 0, 10);
   * }
   */
  const TAU = _PI * 2;

  /**
   * A `Number` constant that's approximately 6.2382.
   *
   * `TWO_PI` is twice the value of the mathematical constant π. It's useful for
   * many tasks that involve rotation and oscillation. For example, calling
   * `rotate(TWO_PI)` rotates the coordinate system `TWO_PI` radians, which is
   * one full turn (360˚). `TWO_PI` and `TAU` are equal.
   *
   * Note: `TWO_PI` radians equals 360˚, `PI` radians equals 180˚, `HALF_PI`
   * radians equals 90˚, and `QUARTER_PI` radians equals 45˚.
   *
   * @property {Number} TWO_PI
   * @final
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Draw an arc from 0 to TWO_PI.
   *   arc(50, 50, 80, 80, 0, TWO_PI);
   *
   *   describe('A white circle drawn on a gray background.');
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Translate the origin to the center.
   *   translate(50, 50);
   *
   *   // Draw a line.
   *   line(0, 0, 40, 0);
   *
   *   // Rotate a full turn.
   *   rotate(TWO_PI);
   *
   *   // Style the second line.
   *   strokeWeight(5);
   *
   *   // Draw the same line, shorter and rotated.
   *   line(0, 0, 20, 0);
   *
   *   describe(
   *     'Two horizontal black lines on a gray background. A thick line extends from the center toward the right. A thin line extends from the end of the thick line.'
   *   );
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   describe(
   *     'A red circle with a blue center oscillates from left to right on a gray background.'
   *   );
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Translate the origin to the center.
   *   translate(50, 50);
   *
   *   // Calculate the x-coordinates.
   *   let x1 = 40 * sin(frameCount * 0.05);
   *   let x2 = 40 * sin(frameCount * 0.05 + TWO_PI);
   *
   *   // Style the oscillators.
   *   noStroke();
   *
   *   // Draw the red oscillator.
   *   fill(255, 0, 0);
   *   circle(x1, 0, 20);
   *
   *   // Draw the blue oscillator, smaller.
   *   fill(0, 0, 255);
   *   circle(x2, 0, 10);
   * }
   */
  const TWO_PI = _PI * 2;

  /**
   * @property {Number} DEG_TO_RAD
   * @final
   */
  const DEG_TO_RAD = _PI / 180.0;

  /**
   * @property {Number} RAD_TO_DEG
   * @final
   */
  const RAD_TO_DEG = 180.0 / _PI;

  // SHAPE
  /**
   * @typedef {'corner'} CORNER
   * @property {CORNER} CORNER
   * @final
   */
  const CORNER = 'corner';
  /**
   * @typedef {'corners'} CORNERS
   * @property {CORNERS} CORNERS
   * @final
   */
  const CORNERS = 'corners';
  /**
   * @typedef {'radius'} RADIUS
   * @property {RADIUS} RADIUS
   * @final
   */
  const RADIUS = 'radius';
  /**
   * @typedef {'right'} RIGHT
   * @property {RIGHT} RIGHT
   * @final
   */
  const RIGHT = 'right';
  /**
   * @typedef {'left'} LEFT
   * @property {LEFT} LEFT
   * @final
   */
  const LEFT = 'left';
  /**
   * @typedef {'center'} CENTER
   * @property {CENTER} CENTER
   * @final
   */
  const CENTER = 'center';
  /**
   * @typedef {'top'} TOP
   * @property {TOP} TOP
   * @final
   */
  const TOP = 'top';
  /**
   * @typedef {'bottom'} BOTTOM
   * @property {BOTTOM} BOTTOM
   * @final
   */
  const BOTTOM = 'bottom';
  /**
   * @typedef {'alphabetic'} BASELINE
   * @property {BASELINE} BASELINE
   * @final
   */
  const BASELINE = 'alphabetic';
  /**
   * @typedef {0x0000} POINTS
   * @property {POINTS} POINTS
   * @final
   */
  const POINTS = 0x0000;
  /**
   * @typedef {0x0001} LINES
   * @property {LINES} LINES
   * @final
   */
  const LINES = 0x0001;
  /**
   * @typedef {0x0003} LINE_STRIP
   * @property {LINE_STRIP} LINE_STRIP
   * @final
   */
  const LINE_STRIP = 0x0003;
  /**
   * @typedef {0x0002} LINE_LOOP
   * @property {LINE_LOOP} LINE_LOOP
   * @final
   */
  const LINE_LOOP = 0x0002;
  /**
   * @typedef {0x0004} TRIANGLES
   * @property {TRIANGLES} TRIANGLES
   * @final
   */
  const TRIANGLES = 0x0004;
  /**
   * @typedef {0x0006} TRIANGLE_FAN
   * @property {TRIANGLE_FAN} TRIANGLE_FAN
   * @final
   */
  const TRIANGLE_FAN = 0x0006;
  /**
   * @typedef {0x0005} TRIANGLE_STRIP
   * @property {TRIANGLE_STRIP} TRIANGLE_STRIP
   * @final
   */
  const TRIANGLE_STRIP = 0x0005;
  /**
   * @typedef {'quads'} QUADS
   * @property {QUADS} QUADS
   * @final
   */
  const QUADS = 'quads';
  /**
   * @typedef {'quad_strip'} QUAD_STRIP
   * @property {QUAD_STRIP} QUAD_STRIP
   * @final
   */
  const QUAD_STRIP = 'quad_strip';
  /**
   * @typedef {'tess'} TESS
   * @property {TESS} TESS
   * @final
   */
  const TESS = 'tess';
  /**
   * @typedef {0x0007} EMPTY_PATH
   * @property {EMPTY_PATH} EMPTY_PATH
   * @final
   */
  const EMPTY_PATH = 0x0007;
  /**
   * @typedef {0x0008} PATH
   * @property {PATH} PATH
   * @final
   */
  const PATH = 0x0008;
  /**
   * @typedef {'close'} CLOSE
   * @property {CLOSE} CLOSE
   * @final
   */
  const CLOSE = 'close';
  /**
   * @typedef {'open'} OPEN
   * @property {OPEN} OPEN
   * @final
   */
  const OPEN = 'open';
  /**
   * @typedef {'chord'} CHORD
   * @property {CHORD} CHORD
   * @final
   */
  const CHORD = 'chord';
  /**
   * @typedef {'pie'} PIE
   * @property {PIE} PIE
   * @final
   */
  const PIE = 'pie';
  /**
   * @typedef {'square'} PROJECT
   * @property {PROJECT} PROJECT
   * @final
   */
  const PROJECT = 'square'; // PEND: careful this is counterintuitive
  /**
   * @typedef {'butt'} SQUARE
   * @property {SQUARE} SQUARE
   * @final
   */
  const SQUARE = 'butt';
  /**
   * @typedef {'round'} ROUND
   * @property {ROUND} ROUND
   * @final
   */
  const ROUND = 'round';
  /**
   * @typedef {'bevel'} BEVEL
   * @property {BEVEL} BEVEL
   * @final
   */
  const BEVEL = 'bevel';
  /**
   * @typedef {'miter'} MITER
   * @property {MITER} MITER
   * @final
   */
  const MITER = 'miter';

  // DOM EXTENSION
  /**
   * AUTO allows us to automatically set the width or height of an element (but not both),
   * based on the current height and width of the element. Only one parameter can
   * be passed to the <a href="/reference/p5.Element/size">size</a> function as AUTO, at a time.
   *
   * @typedef {'auto'} AUTO
   * @property {AUTO} AUTO
   * @final
   */
  const AUTO = 'auto';
  // INPUT
  /**
   * @typedef {'Alt'} ALT
   * @property {ALT} ALT
   * @final
   */
  const ALT = 'Alt';

  /**
   * @typedef {'Backspace'} BACKSPACE
   * @property {BACKSPACE} BACKSPACE
   * @final
   */
  const BACKSPACE = 'Backspace';

  /**
   * @typedef {'Control' | 'Control'} CONTROL
   * @property {CONTROL} CONTROL
   * @final
   */
  const CONTROL = 'Control';

  /**
   * @typedef {'Delete'} DELETE
   * @property {DELETE} DELETE
   * @final
   */
  const DELETE = 'Delete';

  /**
   * @typedef {'ArrowDown'} DOWN_ARROW
   * @property {DOWN_ARROW} DOWN_ARROW
   * @final
   */
  const DOWN_ARROW = 'ArrowDown';

  /**
   * @typedef {'Enter'} ENTER
   * @property {ENTER} ENTER
   * @final
   */
  const ENTER = 'Enter';

  /**
   * @typedef {'Escape'} ESCAPE
   * @property {ESCAPE} ESCAPE
   * @final
   */
  const ESCAPE = 'Escape';

  /**
   * @typedef {'ArrowLeft'} LEFT_ARROW
   * @property {LEFT_ARROW} LEFT_ARROW
   * @final
   */
  const LEFT_ARROW = 'ArrowLeft';

  /**
   * @typedef {'Alt'} OPTION
   * @property {OPTION} OPTION
   * @final
   */
  const OPTION = 'Alt';

  /**
   * @typedef {'Enter'} RETURN
   * @property {RETURN} RETURN
   * @final
   */
  const RETURN = 'Enter';

  /**
   * @typedef {'ArrowRight'} RIGHT_ARROW
   * @property {RIGHT_ARROW} RIGHT_ARROW
   * @final
   */
  const RIGHT_ARROW = 'ArrowRight';

  /**
   * @typedef {'Shift'} SHIFT
   * @property {SHIFT} SHIFT
   * @final
   */
  const SHIFT = 'Shift';

  /**
   * @typedef {'Tab'} TAB
   * @property {TAB} TAB
   * @final
   */
  const TAB = 'Tab';

  /**
   * @typedef {'ArrowUp'} UP_ARROW
   * @property {UP_ARROW} UP_ARROW
   * @final
   */
  const UP_ARROW = 'ArrowUp';

  // RENDERING
  /**
   * @typedef {'source-over'} BLEND
   * @property {BLEND} BLEND
   * @final
   */
  const BLEND = 'source-over';
  /**
   * @typedef {'destination-out'} REMOVE
   * @property {REMOVE} REMOVE
   * @final
   */
  const REMOVE = 'destination-out';
  /**
   * @typedef {'lighter'} ADD
   * @property {ADD} ADD
   * @final
   */
  const ADD = 'lighter';
  /**
   * @typedef {'darken'} DARKEST
   * @property {DARKEST} DARKEST
   * @final
   */
  const DARKEST = 'darken';
  /**
   * @typedef {'lighten'} LIGHTEST
   * @property {LIGHTEST} LIGHTEST
   * @final
   */
  const LIGHTEST = 'lighten';
  /**
   * @typedef {'difference'} DIFFERENCE
   * @property {DIFFERENCE} DIFFERENCE
   * @final
   */
  const DIFFERENCE = 'difference';
  /**
   * @typedef {'subtract'} SUBTRACT
   * @property {SUBTRACT} SUBTRACT
   * @final
   */
  const SUBTRACT = 'subtract';
  /**
   * @typedef {'exclusion'} EXCLUSION
   * @property {EXCLUSION} EXCLUSION
   * @final
   */
  const EXCLUSION = 'exclusion';
  /**
   * @typedef {'multiply'} MULTIPLY
   * @property {MULTIPLY} MULTIPLY
   * @final
   */
  const MULTIPLY = 'multiply';
  /**
   * @typedef {'screen'} SCREEN
   * @property {SCREEN} SCREEN
   * @final
   */
  const SCREEN = 'screen';
  /**
   * @typedef {'copy'} REPLACE
   * @property {REPLACE} REPLACE
   * @final
   */
  const REPLACE = 'copy';
  /**
   * @typedef {'overlay'} OVERLAY
   * @property {OVERLAY} OVERLAY
   * @final
   */
  const OVERLAY = 'overlay';
  /**
   * @typedef {'hard-light'} HARD_LIGHT
   * @property {HARD_LIGHT} HARD_LIGHT
   * @final
   */
  const HARD_LIGHT = 'hard-light';
  /**
   * @typedef {'soft-light'} SOFT_LIGHT
   * @property {SOFT_LIGHT} SOFT_LIGHT
   * @final
   */
  const SOFT_LIGHT = 'soft-light';
  /**
   * @typedef {'color-dodge'} DODGE
   * @property {DODGE} DODGE
   * @final
   */
  const DODGE = 'color-dodge';
  /**
   * @typedef {'color-burn'} BURN
   * @property {BURN} BURN
   * @final
   */
  const BURN = 'color-burn';

  // FILTERS
  /**
   * @typedef {'threshold'} THRESHOLD
   * @property {THRESHOLD} THRESHOLD
   * @final
   */
  const THRESHOLD = 'threshold';
  /**
   * @typedef {'gray'} GRAY
   * @property {GRAY} GRAY
   * @final
   */
  const GRAY = 'gray';
  /**
   * @typedef {'opaque'} OPAQUE
   * @property {OPAQUE} OPAQUE
   * @final
   */
  const OPAQUE = 'opaque';
  /**
   * @typedef {'invert'} INVERT
   * @property {INVERT} INVERT
   * @final
   */
  const INVERT = 'invert';
  /**
   * @typedef {'posterize'} POSTERIZE
   * @property {POSTERIZE} POSTERIZE
   * @final
   */
  const POSTERIZE = 'posterize';
  /**
   * @typedef {'dilate'} DILATE
   * @property {DILATE} DILATE
   * @final
   */
  const DILATE = 'dilate';
  /**
   * @typedef {'erode'} ERODE
   * @property {ERODE} ERODE
   * @final
   */
  const ERODE = 'erode';
  /**
   * @typedef {'blur'} BLUR
   * @property {BLUR} BLUR
   * @final
   */
  const BLUR = 'blur';

  // TYPOGRAPHY
  /**
   * @typedef {'normal'} NORMAL
   * @property {NORMAL} NORMAL
   * @final
   */
  const NORMAL = 'normal';
  /**
   * @typedef {'italic'} ITALIC
   * @property {ITALIC} ITALIC
   * @final
   */
  const ITALIC = 'italic';
  /**
   * @typedef {'bold'} BOLD
   * @property {BOLD} BOLD
   * @final
   */
  const BOLD = 'bold';
  /**
   * @typedef {'bold italic'} BOLDITALIC
   * @property {BOLDITALIC} BOLDITALIC
   * @final
   */
  const BOLDITALIC = 'bold italic';
  /**
   * @typedef {'CHAR'} CHAR
   * @property {CHAR} CHAR
   * @final
   */
  const CHAR = 'CHAR';
  /**
   * @typedef {'WORD'} WORD
   * @property {WORD} WORD
   * @final
   */
  const WORD = 'WORD';

  // TYPOGRAPHY-INTERNAL
  const _DEFAULT_TEXT_FILL = '#000000';
  const _DEFAULT_LEADMULT = 1.25;
  const _CTX_MIDDLE = 'middle';

  // VERTICES
  /**
   * @typedef {'linear'} LINEAR
   * @property {LINEAR} LINEAR
   * @final
   */
  const LINEAR = 'linear';
  /**
   * @typedef {'quadratic'} QUADRATIC
   * @property {QUADRATIC} QUADRATIC
   * @final
   */
  const QUADRATIC = 'quadratic';
  /**
   * @typedef {'bezier'} BEZIER
   * @property {BEZIER} BEZIER
   * @final
   */
  const BEZIER = 'bezier';
  /**
   * @typedef {'curve'} CURVE
   * @property {CURVE} CURVE
   * @final
   */
  const CURVE = 'curve';

  // WEBGL DRAWMODES
  /**
   * @typedef {'stroke'} STROKE
   * @property {STROKE} STROKE
   * @final
   */
  const STROKE = 'stroke';
  /**
   * @typedef {'fill'} FILL
   * @property {FILL} FILL
   * @final
   */
  const FILL = 'fill';
  /**
   * @typedef {'texture'} TEXTURE
   * @property {TEXTURE} TEXTURE
   * @final
   */
  const TEXTURE = 'texture';
  /**
   * @typedef {'immediate'} IMMEDIATE
   * @property {IMMEDIATE} IMMEDIATE
   * @final
   */
  const IMMEDIATE = 'immediate';

  // WEBGL TEXTURE MODE
  // NORMAL already exists for typography
  /**
   * @typedef {'image'} IMAGE
   * @property {IMAGE} IMAGE
   * @final
   */
  const IMAGE = 'image';

  // WEBGL TEXTURE WRAP AND FILTERING
  // LINEAR already exists above
  /**
   * @typedef {'linear_mipmap'} LINEAR_MIPMAP
   * @property {LINEAR_MIPMAP} LINEAR_MIPMAP
   * @final
   * @private
   */
  const LINEAR_MIPMAP = 'linear_mipmap';
  /**
   * @typedef {'nearest'} NEAREST
   * @property {NEAREST} NEAREST
   * @final
   */
  const NEAREST = 'nearest';
  /**
   * @typedef {'repeat'} REPEAT
   * @property {REPEAT} REPEAT
   * @final
   */
  const REPEAT = 'repeat';
  /**
   * @typedef {'clamp'} CLAMP
   * @property {CLAMP} CLAMP
   * @final
   */
  const CLAMP = 'clamp';
  /**
   * @typedef {'mirror'} MIRROR
   * @property {MIRROR} MIRROR
   * @final
   */
  const MIRROR = 'mirror';

  // WEBGL GEOMETRY SHADING
  /**
   * @typedef {'flat'} FLAT
   * @property {FLAT} FLAT
   * @final
   */
  const FLAT = 'flat';
  /**
   * @typedef {'smooth'} SMOOTH
   * @property {SMOOTH} SMOOTH
   * @final
   */
  const SMOOTH = 'smooth';

  // DEVICE-ORIENTATION
  /**
   * @typedef {'landscape'} LANDSCAPE
   * @property {LANDSCAPE} LANDSCAPE
   * @final
   */
  const LANDSCAPE = 'landscape';
  /**
   * @typedef {'portrait'} PORTRAIT
   * @property {PORTRAIT} PORTRAIT
   * @final
   */
  const PORTRAIT = 'portrait';

  // DEFAULTS
  const _DEFAULT_STROKE = '#000000';
  const _DEFAULT_FILL = '#FFFFFF';

  /**
   * @typedef {'grid'} GRID
   * @property {GRID} GRID
   * @final
   */
  const GRID = 'grid';

  /**
   * @typedef {'axes'} AXES
   * @property {AXES} AXES
   * @final
   */
  const AXES = 'axes';

  /**
   * @typedef {'label'} LABEL
   * @property {LABEL} LABEL
   * @final
   */
  const LABEL = 'label';
  /**
   * @typedef {'fallback'} FALLBACK
   * @property {FALLBACK} FALLBACK
   * @final
   */
  const FALLBACK = 'fallback';

  /**
   * @typedef {'contain'} CONTAIN
   * @property {CONTAIN} CONTAIN
   * @final
   */
  const CONTAIN = 'contain';

  /**
   * @typedef {'cover'} COVER
   * @property {COVER} COVER
   * @final
   */
  const COVER = 'cover';

  /**
   * @typedef {'unsigned-byte'} UNSIGNED_BYTE
   * @property {UNSIGNED_BYTE} UNSIGNED_BYTE
   * @final
   */
  const UNSIGNED_BYTE = 'unsigned-byte';

  /**
   * @typedef {'unsigned-int'} UNSIGNED_INT
   * @property {UNSIGNED_INT} UNSIGNED_INT
   * @final
   */
  const UNSIGNED_INT = 'unsigned-int';

  /**
   * @typedef {'float'} FLOAT
   * @property {FLOAT} FLOAT
   * @final
   */
  const FLOAT = 'float';

  /**
   * @typedef {'half-float'} HALF_FLOAT
   * @property {HALF_FLOAT} HALF_FLOAT
   * @final
   */
  const HALF_FLOAT = 'half-float';

  /**
   * The `splineProperty('ends')` mode where splines curve through
   * their first and last points.
   * @typedef {unique symbol} INCLUDE
   * @property {INCLUDE} INCLUDE
   * @final
   */
  const INCLUDE = Symbol('include');

  /**
   * The `splineProperty('ends')` mode where the first and last points in a spline
   * affect the direction of the curve, but are not rendered.
   * @typedef {unique symbol} EXCLUDE
   * @property {EXCLUDE} EXCLUDE
   * @final
   */
  const EXCLUDE = Symbol('exclude');

  /**
   * The `splineProperty('ends')` mode where the spline loops back to its first point.
   * Only used internally.
   * @typedef {unique symbol} JOIN
   * @property {JOIN} JOIN
   * @final
   * @private
   */
  const JOIN = Symbol('join');

  var constants = /*#__PURE__*/Object.freeze({
    __proto__: null,
    ADD: ADD,
    ALT: ALT,
    ARROW: ARROW,
    AUTO: AUTO,
    AXES: AXES,
    BACKSPACE: BACKSPACE,
    BASELINE: BASELINE,
    BEVEL: BEVEL,
    BEZIER: BEZIER,
    BLEND: BLEND,
    BLUR: BLUR,
    BOLD: BOLD,
    BOLDITALIC: BOLDITALIC,
    BOTTOM: BOTTOM,
    BURN: BURN,
    CENTER: CENTER,
    CHAR: CHAR,
    CHORD: CHORD,
    CLAMP: CLAMP,
    CLOSE: CLOSE,
    CONTAIN: CONTAIN,
    CONTROL: CONTROL,
    CORNER: CORNER,
    CORNERS: CORNERS,
    COVER: COVER,
    CROSS: CROSS,
    CURVE: CURVE,
    DARKEST: DARKEST,
    DEG_TO_RAD: DEG_TO_RAD,
    DELETE: DELETE,
    DIFFERENCE: DIFFERENCE,
    DILATE: DILATE,
    DODGE: DODGE,
    DOWN_ARROW: DOWN_ARROW,
    EMPTY_PATH: EMPTY_PATH,
    ENTER: ENTER,
    ERODE: ERODE,
    ESCAPE: ESCAPE,
    EXCLUDE: EXCLUDE,
    EXCLUSION: EXCLUSION,
    FALLBACK: FALLBACK,
    FILL: FILL,
    FLAT: FLAT,
    FLOAT: FLOAT,
    FULL: FULL,
    GRAY: GRAY,
    GRID: GRID,
    HALF_FLOAT: HALF_FLOAT,
    HALF_PI: HALF_PI,
    HAND: HAND,
    HARD_LIGHT: HARD_LIGHT,
    IMAGE: IMAGE,
    IMMEDIATE: IMMEDIATE,
    INCLUDE: INCLUDE,
    INVERT: INVERT,
    ITALIC: ITALIC,
    JOIN: JOIN,
    LABEL: LABEL,
    LANDSCAPE: LANDSCAPE,
    LEFT: LEFT,
    LEFT_ARROW: LEFT_ARROW,
    LIGHTEST: LIGHTEST,
    LINEAR: LINEAR,
    LINEAR_MIPMAP: LINEAR_MIPMAP,
    LINES: LINES,
    LINE_LOOP: LINE_LOOP,
    LINE_STRIP: LINE_STRIP,
    MIRROR: MIRROR,
    MITER: MITER,
    MOVE: MOVE,
    MULTIPLY: MULTIPLY,
    NEAREST: NEAREST,
    NORMAL: NORMAL,
    OPAQUE: OPAQUE,
    OPEN: OPEN,
    OPTION: OPTION,
    OVERLAY: OVERLAY,
    P2D: P2D,
    P2DHDR: P2DHDR,
    PATH: PATH,
    PI: PI,
    PIE: PIE,
    POINTS: POINTS,
    PORTRAIT: PORTRAIT,
    POSTERIZE: POSTERIZE,
    PROJECT: PROJECT,
    QUADRATIC: QUADRATIC,
    QUADS: QUADS,
    QUAD_STRIP: QUAD_STRIP,
    QUARTER_PI: QUARTER_PI,
    RADIUS: RADIUS,
    RAD_TO_DEG: RAD_TO_DEG,
    REMOVE: REMOVE,
    REPEAT: REPEAT,
    REPLACE: REPLACE,
    RETURN: RETURN,
    RIGHT: RIGHT,
    RIGHT_ARROW: RIGHT_ARROW,
    ROUND: ROUND,
    SCREEN: SCREEN,
    SHIFT: SHIFT,
    SIMPLE: SIMPLE,
    SMOOTH: SMOOTH,
    SOFT_LIGHT: SOFT_LIGHT,
    SQUARE: SQUARE,
    STROKE: STROKE,
    SUBTRACT: SUBTRACT,
    TAB: TAB,
    TAU: TAU,
    TESS: TESS,
    TEXT: TEXT,
    TEXTURE: TEXTURE,
    THRESHOLD: THRESHOLD,
    TOP: TOP,
    TRIANGLES: TRIANGLES,
    TRIANGLE_FAN: TRIANGLE_FAN,
    TRIANGLE_STRIP: TRIANGLE_STRIP,
    TWO_PI: TWO_PI,
    UNSIGNED_BYTE: UNSIGNED_BYTE,
    UNSIGNED_INT: UNSIGNED_INT,
    UP_ARROW: UP_ARROW,
    VERSION: VERSION,
    WAIT: WAIT,
    WEBGL: WEBGL,
    WEBGL2: WEBGL2,
    WEBGPU: WEBGPU,
    WORD: WORD,
    _CTX_MIDDLE: _CTX_MIDDLE,
    _DEFAULT_FILL: _DEFAULT_FILL,
    _DEFAULT_LEADMULT: _DEFAULT_LEADMULT,
    _DEFAULT_STROKE: _DEFAULT_STROKE,
    _DEFAULT_TEXT_FILL: _DEFAULT_TEXT_FILL
  });

  /**
   * @module Transform
   * @submodule Transform
   * @for p5
   * @requires core
   * @requires constants
   */

  function transform$1(p5, fn){
    /**
     * Applies a transformation matrix to the coordinate system.
     *
     * Transformations such as
     * <a href="#/p5/translate">translate()</a>,
     * <a href="#/p5/rotate">rotate()</a>, and
     * <a href="#/p5/scale">scale()</a>
     * use matrix-vector multiplication behind the scenes. A table of numbers,
     * called a matrix, encodes each transformation. The values in the matrix
     * then multiply each point on the canvas, which is represented by a vector.
     *
     * `applyMatrix()` allows for many transformations to be applied at once. See
     * <a href="https://en.wikipedia.org/wiki/Transformation_matrix" target="_blank">Wikipedia</a>
     * and <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Matrix_math_for_the_web" target="_blank">MDN</a>
     * for more details about transformations.
     *
     * There are two ways to call `applyMatrix()` in two and three dimensions.
     *
     * In 2D mode, the parameters `a`, `b`, `c`, `d`, `e`, and `f`, correspond to
     * elements in the following transformation matrix:
     *
     * > <img style="max-width: 150px" src="assets/transformation-matrix.png"
     * alt="The transformation matrix used when applyMatrix is called in 2D mode."/>
     *
     * The numbers can be passed individually, as in
     * `applyMatrix(2, 0, 0, 0, 2, 0)`. They can also be passed in an array, as in
     * `applyMatrix([2, 0, 0, 0, 2, 0])`.
     *
     * In 3D mode, the parameters `a`, `b`, `c`, `d`, `e`, `f`, `g`, `h`, `i`,
     * `j`, `k`, `l`, `m`, `n`, `o`, and `p` correspond to elements in the
     * following transformation matrix:
     *
     * <img style="max-width: 300px" src="assets/transformation-matrix-4-4.png"
     * alt="The transformation matrix used when applyMatrix is called in 3D mode."/>
     *
     * The numbers can be passed individually, as in
     * `applyMatrix(2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1)`. They can
     * also be passed in an array, as in
     * `applyMatrix([2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1])`.
     *
     * By default, transformations accumulate. The
     * <a href="#/p5/push">push()</a> and <a href="#/p5/pop">pop()</a> functions
     * can be used to isolate transformations within distinct drawing groups.
     *
     * Note: Transformations are reset at the beginning of the draw loop. Calling
     * `applyMatrix()` inside the <a href="#/p5/draw">draw()</a> function won't
     * cause shapes to transform continuously.
     *
     * @method applyMatrix
     * @param  {Array<Number>} arr an array containing the elements of the transformation matrix. Its length should be either 6 (2D) or 16 (3D).
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A white circle on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Translate the origin to the center.
     *   applyMatrix(1, 0, 0, 1, 50, 50);
     *
     *   // Draw the circle at coordinates (0, 0).
     *   circle(0, 0, 40);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A white circle on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Translate the origin to the center.
     *   let m = [1, 0, 0, 1, 50, 50];
     *   applyMatrix(m);
     *
     *   // Draw the circle at coordinates (0, 0).
     *   circle(0, 0, 40);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe("A white rectangle on a gray background. The rectangle's long axis runs from top-left to bottom-right.");
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Rotate the coordinate system 1/8 turn.
     *   let angle = QUARTER_PI;
     *   let ca = cos(angle);
     *   let sa = sin(angle);
     *   applyMatrix(ca, sa, -sa, ca, 0, 0);
     *
     *   // Draw a rectangle at coordinates (50, 0).
     *   rect(50, 0, 40, 20);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'Two white squares on a gray background. The larger square appears at the top-center. The smaller square appears at the top-left.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw a square at (30, 20).
     *   square(30, 20, 40);
     *
     *   // Scale the coordinate system by a factor of 0.5.
     *   applyMatrix(0.5, 0, 0, 0.5, 0, 0);
     *
     *   // Draw a square at (30, 20).
     *   // It appears at (15, 10) after scaling.
     *   square(30, 20, 40);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A white quadrilateral on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the shear factor.
     *   let angle = QUARTER_PI;
     *   let shearFactor = 1 / tan(HALF_PI - angle);
     *
     *   // Shear the coordinate system along the x-axis.
     *   applyMatrix(1, 0, shearFactor, 1, 0, 0);
     *
     *   // Draw the square.
     *   square(0, 0, 50);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cube rotates slowly against a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Rotate the coordinate system a little more each frame.
     *   let angle = frameCount * 0.01;
     *   let ca = cos(angle);
     *   let sa = sin(angle);
     *   applyMatrix(ca, 0, sa, 0, 0, 1, 0, 0, -sa, 0, ca, 0, 0, 0, 0, 1);
     *
     *   // Draw a box.
     *   box();
     * }
     */
    /**
     * @method applyMatrix
     * @param  {Number} a an element of the transformation matrix.
     * @param  {Number} b an element of the transformation matrix.
     * @param  {Number} c an element of the transformation matrix.
     * @param  {Number} d an element of the transformation matrix.
     * @param  {Number} e an element of the transformation matrix.
     * @param  {Number} f an element of the transformation matrix.
     * @chainable
     */
    /**
     * @method applyMatrix
     * @param  {Number} a
     * @param  {Number} b
     * @param  {Number} c
     * @param  {Number} d
     * @param  {Number} e
     * @param  {Number} f
     * @param  {Number} g an element of the transformation matrix.
     * @param  {Number} h an element of the transformation matrix.
     * @param  {Number} i an element of the transformation matrix.
     * @param  {Number} j an element of the transformation matrix.
     * @param  {Number} k an element of the transformation matrix.
     * @param  {Number} l an element of the transformation matrix.
     * @param  {Number} m an element of the transformation matrix.
     * @param  {Number} n an element of the transformation matrix.
     * @param  {Number} o an element of the transformation matrix.
     * @param  {Number} p an element of the transformation matrix.
     * @chainable
     */
    fn.applyMatrix = function(...args) {
      let isTypedArray = args[0] instanceof Object.getPrototypeOf(Uint8Array);
      if (Array.isArray(args[0]) || isTypedArray) {
        this._renderer.applyMatrix(...args[0]);
      } else {
        this._renderer.applyMatrix(...args);
      }
      return this;
    };

    /**
     * Clears all transformations applied to the coordinate system.
     *
     * @method resetMatrix
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'Two circles drawn on a gray background. A blue circle is at the top-left and a red circle is at the bottom-right.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Translate the origin to the center.
     *   translate(50, 50);
     *
     *   // Draw a blue circle at the coordinates (25, 25).
     *   fill('blue');
     *   circle(25, 25, 20);
     *
     *   // Clear all transformations.
     *   // The origin is now at the top-left corner.
     *   resetMatrix();
     *
     *   // Draw a red circle at the coordinates (25, 25).
     *   fill('red');
     *   circle(25, 25, 20);
     * }
     */
    fn.resetMatrix = function() {
      this._renderer.resetMatrix();
      return this;
    };

    /**
     * Rotates the coordinate system.
     *
     * By default, the positive x-axis points to the right and the positive y-axis
     * points downward. The `rotate()` function changes this orientation by
     * rotating the coordinate system about the origin. Everything drawn after
     * `rotate()` is called will appear to be rotated.
     *
     * The first parameter, `angle`, is the amount to rotate. For example, calling
     * `rotate(1)` rotates the coordinate system clockwise 1 radian which is
     * nearly 57˚. `rotate()` interprets angle values using the current
     * <a href="#/p5/angleMode">angleMode()</a>.
     *
     * The second parameter, `axis`, is optional. It's used to orient 3D rotations
     * in WebGL mode. If a <a href="#/p5.Vector">p5.Vector</a> is passed, as in
     * `rotate(QUARTER_PI, myVector)`, then the coordinate system will rotate
     * `QUARTER_PI` radians about `myVector`. If an array of vector components is
     * passed, as in `rotate(QUARTER_PI, [1, 0, 0])`, then the coordinate system
     * will rotate `QUARTER_PI` radians about a vector with the components
     * `[1, 0, 0]`.
     *
     * By default, transformations accumulate. For example, calling `rotate(1)`
     * twice has the same effect as calling `rotate(2)` once. The
     * <a href="#/p5/push">push()</a> and <a href="#/p5/pop">pop()</a> functions
     * can be used to isolate transformations within distinct drawing groups.
     *
     * Note: Transformations are reset at the beginning of the draw loop. Calling
     * `rotate(1)` inside the <a href="#/p5/draw">draw()</a> function won't cause
     * shapes to spin.
     *
     * @method rotate
     * @param  {Number} angle angle of rotation in the current <a href="#/p5/angleMode">angleMode()</a>.
     * @param  {p5.Vector|Number[]} [axis] axis to rotate about in 3D.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     "A white rectangle on a gray background. The rectangle's long axis runs from top-left to bottom-right."
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Rotate the coordinate system 1/8 turn.
     *   rotate(QUARTER_PI);
     *
     *   // Draw a rectangle at coordinates (50, 0).
     *   rect(50, 0, 40, 20);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     "A white rectangle on a gray background. The rectangle's long axis runs from top-left to bottom-right."
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Rotate the coordinate system 1/16 turn.
     *   rotate(QUARTER_PI / 2);
     *
     *   // Rotate the coordinate system another 1/16 turn.
     *   rotate(QUARTER_PI / 2);
     *
     *   // Draw a rectangle at coordinates (50, 0).
     *   rect(50, 0, 40, 20);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   describe(
     *     "A white rectangle on a gray background. The rectangle's long axis runs from top-left to bottom-right."
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Rotate the coordinate system 1/8 turn.
     *   rotate(45);
     *
     *   // Draw a rectangle at coordinates (50, 0).
     *   rect(50, 0, 40, 20);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A white rectangle on a gray background. The rectangle rotates slowly about the top-left corner. It disappears and reappears periodically.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Rotate the coordinate system a little more each frame.
     *   let angle = frameCount * 0.01;
     *   rotate(angle);
     *
     *   // Draw a rectangle at coordinates (50, 0).
     *   rect(50, 0, 40, 20);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe("A cube on a gray background. The cube's front face points to the top-right.");
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Rotate the coordinate system 1/8 turn about
     *   // the axis [1, 1, 0].
     *   let axis = createVector(1, 1, 0);
     *   rotate(QUARTER_PI, axis);
     *
     *   // Draw a box.
     *   box();
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe("A cube on a gray background. The cube's front face points to the top-right.");
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Rotate the coordinate system 1/8 turn about
     *   // the axis [1, 1, 0].
     *   let axis = [1, 1, 0];
     *   rotate(QUARTER_PI, axis);
     *
     *   // Draw a box.
     *   box();
     * }
     */
    fn.rotate = function(angle, axis) {
      // p5._validateParameters('rotate', arguments);
      this._renderer.rotate(this._toRadians(angle), axis);
      return this;
    };

    /**
     * Rotates the coordinate system about the x-axis in WebGL mode.
     *
     * The parameter, `angle`, is the amount to rotate. For example, calling
     * `rotateX(1)` rotates the coordinate system about the x-axis by 1 radian.
     * `rotateX()` interprets angle values using the current
     * <a href="#/p5/angleMode">angleMode()</a>.
     *
     * By default, transformations accumulate. For example, calling `rotateX(1)`
     * twice has the same effect as calling `rotateX(2)` once. The
     * <a href="#/p5/push">push()</a> and <a href="#/p5/pop">pop()</a> functions
     * can be used to isolate transformations within distinct drawing groups.
     *
     * Note: Transformations are reset at the beginning of the draw loop. Calling
     * `rotateX(1)` inside the <a href="#/p5/draw">draw()</a> function won't cause
     * shapes to spin.
     *
     * @method  rotateX
     * @param  {Number} angle angle of rotation in the current <a href="#/p5/angleMode">angleMode()</a>.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cube on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Rotate the coordinate system 1/8 turn.
     *   rotateX(QUARTER_PI);
     *
     *   // Draw a box.
     *   box();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cube on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Rotate the coordinate system 1/16 turn.
     *   rotateX(QUARTER_PI / 2);
     *
     *   // Rotate the coordinate system 1/16 turn.
     *   rotateX(QUARTER_PI / 2);
     *
     *   // Draw a box.
     *   box();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   describe('A white cube on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Rotate the coordinate system 1/8 turn.
     *   rotateX(45);
     *
     *   // Draw a box.
     *   box();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cube rotates slowly against a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Rotate the coordinate system a little more each frame.
     *   let angle = frameCount * 0.01;
     *   rotateX(angle);
     *
     *   // Draw a box.
     *   box();
     * }
     */
    fn.rotateX = function(angle) {
      this._assert3d('rotateX');
      // p5._validateParameters('rotateX', arguments);
      this._renderer.rotateX(this._toRadians(angle));
      return this;
    };

    /**
     * Rotates the coordinate system about the y-axis in WebGL mode.
     *
     * The parameter, `angle`, is the amount to rotate. For example, calling
     * `rotateY(1)` rotates the coordinate system about the y-axis by 1 radian.
     * `rotateY()` interprets angle values using the current
     * <a href="#/p5/angleMode">angleMode()</a>.
     *
     * By default, transformations accumulate. For example, calling `rotateY(1)`
     * twice has the same effect as calling `rotateY(2)` once. The
     * <a href="#/p5/push">push()</a> and <a href="#/p5/pop">pop()</a> functions
     * can be used to isolate transformations within distinct drawing groups.
     *
     * Note: Transformations are reset at the beginning of the draw loop. Calling
     * `rotateY(1)` inside the <a href="#/p5/draw">draw()</a> function won't cause
     * shapes to spin.
     *
     * @method rotateY
     * @param  {Number} angle angle of rotation in the current <a href="#/p5/angleMode">angleMode()</a>.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cube on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Rotate the coordinate system 1/8 turn.
     *   rotateY(QUARTER_PI);
     *
     *   // Draw a box.
     *   box();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cube on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Rotate the coordinate system 1/16 turn.
     *   rotateY(QUARTER_PI / 2);
     *
     *   // Rotate the coordinate system 1/16 turn.
     *   rotateY(QUARTER_PI / 2);
     *
     *   // Draw a box.
     *   box();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   describe('A white cube on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Rotate the coordinate system 1/8 turn.
     *   rotateY(45);
     *
     *   // Draw a box.
     *   box();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cube rotates slowly against a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Rotate the coordinate system a little more each frame.
     *   let angle = frameCount * 0.01;
     *   rotateY(angle);
     *
     *   // Draw a box.
     *   box();
     * }
     */
    fn.rotateY = function(angle) {
      this._assert3d('rotateY');
      // p5._validateParameters('rotateY', arguments);
      this._renderer.rotateY(this._toRadians(angle));
      return this;
    };

    /**
     * Rotates the coordinate system about the z-axis in WebGL mode.
     *
     * The parameter, `angle`, is the amount to rotate. For example, calling
     * `rotateZ(1)` rotates the coordinate system about the z-axis by 1 radian.
     * `rotateZ()` interprets angle values using the current
     * <a href="#/p5/angleMode">angleMode()</a>.
     *
     * By default, transformations accumulate. For example, calling `rotateZ(1)`
     * twice has the same effect as calling `rotateZ(2)` once. The
     * <a href="#/p5/push">push()</a> and <a href="#/p5/pop">pop()</a> functions
     * can be used to isolate transformations within distinct drawing groups.
     *
     * Note: Transformations are reset at the beginning of the draw loop. Calling
     * `rotateZ(1)` inside the <a href="#/p5/draw">draw()</a> function won't cause
     * shapes to spin.
     *
     * @method rotateZ
     * @param  {Number} angle angle of rotation in the current <a href="#/p5/angleMode">angleMode()</a>.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cube on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Rotate the coordinate system 1/8 turn.
     *   rotateZ(QUARTER_PI);
     *
     *   // Draw a box.
     *   box();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cube on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Rotate the coordinate system 1/16 turn.
     *   rotateZ(QUARTER_PI / 2);
     *
     *   // Rotate the coordinate system 1/16 turn.
     *   rotateZ(QUARTER_PI / 2);
     *
     *   // Draw a box.
     *   box();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   describe('A white cube on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Rotate the coordinate system 1/8 turn.
     *   rotateZ(45);
     *
     *   // Draw a box.
     *   box();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cube rotates slowly against a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Rotate the coordinate system a little more each frame.
     *   let angle = frameCount * 0.01;
     *   rotateZ(angle);
     *
     *   // Draw a box.
     *   box();
     * }
     */
    fn.rotateZ = function(angle) {
      this._assert3d('rotateZ');
      // p5._validateParameters('rotateZ', arguments);
      this._renderer.rotateZ(this._toRadians(angle));
      return this;
    };

    /**
     * Scales the coordinate system.
     *
     * By default, shapes are drawn at their original scale. A rectangle that's 50
     * pixels wide appears to take up half the width of a 100 pixel-wide canvas.
     * The `scale()` function can shrink or stretch the coordinate system so that
     * shapes appear at different sizes. There are two ways to call `scale()` with
     * parameters that set the scale factor(s).
     *
     * The first way to call `scale()` uses numbers to set the amount of scaling.
     * The first parameter, `s`, sets the amount to scale each axis. For example,
     * calling `scale(2)` stretches the x-, y-, and z-axes by a factor of 2. The
     * next two parameters, `y` and `z`, are optional. They set the amount to
     * scale the y- and z-axes. For example, calling `scale(2, 0.5, 1)` stretches
     * the x-axis by a factor of 2, shrinks the y-axis by a factor of 0.5, and
     * leaves the z-axis unchanged.
     *
     * The second way to call `scale()` uses a <a href="#/p5.Vector">p5.Vector</a>
     * object to set the scale factors. For example, calling `scale(myVector)`
     * uses the x-, y-, and z-components of `myVector` to set the amount of
     * scaling along the x-, y-, and z-axes. Doing so is the same as calling
     * `scale(myVector.x, myVector.y, myVector.z)`.
     *
     * By default, transformations accumulate. For example, calling `scale(1)`
     * twice has the same effect as calling `scale(2)` once. The
     * <a href="#/p5/push">push()</a> and <a href="#/p5/pop">pop()</a> functions
     * can be used to isolate transformations within distinct drawing groups.
     *
     * Note: Transformations are reset at the beginning of the draw loop. Calling
     * `scale(2)` inside the <a href="#/p5/draw">draw()</a> function won't cause
     * shapes to grow continuously.
     *
     * @method scale
     * @param  {Number|p5.Vector|Number[]} s amount to scale along the positive x-axis.
     * @param  {Number} [y] amount to scale along the positive y-axis. Defaults to `s`.
     * @param  {Number} [z] amount to scale along the positive z-axis. Defaults to `y`.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'Two white squares on a gray background. The larger square appears at the top-center. The smaller square appears at the top-left.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw a square at (30, 20).
     *   square(30, 20, 40);
     *
     *   // Scale the coordinate system by a factor of 0.5.
     *   scale(0.5);
     *
     *   // Draw a square at (30, 20).
     *   // It appears at (15, 10) after scaling.
     *   square(30, 20, 40);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A rectangle and a square drawn in white on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw a square at (30, 20).
     *   square(30, 20, 40);
     *
     *   // Scale the coordinate system by factors of
     *   // 0.5 along the x-axis and
     *   // 1.3 along the y-axis.
     *   scale(0.5, 1.3);
     *
     *   // Draw a square at (30, 20).
     *   // It appears as a rectangle at (15, 26) after scaling.
     *   square(30, 20, 40);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A rectangle and a square drawn in white on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw a square at (30, 20).
     *   square(30, 20, 40);
     *
     *   // Create a p5.Vector object.
     *   let v = createVector(0.5, 1.3);
     *
     *   // Scale the coordinate system by factors of
     *   // 0.5 along the x-axis and
     *   // 1.3 along the y-axis.
     *   scale(v);
     *
     *   // Draw a square at (30, 20).
     *   // It appears as a rectangle at (15, 26) after scaling.
     *   square(30, 20, 40);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     'A red box and a blue box drawn on a gray background. The red box appears embedded in the blue box.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Style the spheres.
     *   noStroke();
     *
     *   // Draw the red sphere.
     *   fill('red');
     *   box();
     *
     *   // Scale the coordinate system by factors of
     *   // 0.5 along the x-axis and
     *   // 1.3 along the y-axis and
     *   // 2 along the z-axis.
     *   scale(0.5, 1.3, 2);
     *
     *   // Draw the blue sphere.
     *   fill('blue');
     *   box();
     * }
     */
    /**
     * @method scale
     * @param  {p5.Vector|Number[]} scales vector whose components should be used to scale.
     * @chainable
     */
    fn.scale = function(x, y, z) {
      // p5._validateParameters('scale', arguments);
      // Only check for Vector argument type if Vector is available
      if (x instanceof p5.Vector) {
        const v = x;
        x = v.x;
        y = v.y;
        z = v.z;
      } else if (Array.isArray(x)) {
        const rg = x;
        x = rg[0];
        y = rg[1];
        z = rg[2] || 1;
      }
      if (isNaN(y)) {
        y = z = x;
      } else if (isNaN(z)) {
        z = 1;
      }

      this._renderer.scale(x, y, z);

      return this;
    };

    /**
     * Shears the x-axis so that shapes appear skewed.
     *
     * By default, the x- and y-axes are perpendicular. The `shearX()` function
     * transforms the coordinate system so that x-coordinates are translated while
     * y-coordinates are fixed.
     *
     * The first parameter, `angle`, is the amount to shear. For example, calling
     * `shearX(1)` transforms all x-coordinates using the formula
     * `x = x + y * tan(angle)`. `shearX()` interprets angle values using the
     * current <a href="#/p5/angleMode">angleMode()</a>.
     *
     * By default, transformations accumulate. For example, calling
     * `shearX(1)` twice has the same effect as calling `shearX(2)` once. The
     * <a href="#/p5/push">push()</a> and
     * <a href="#/p5/pop">pop()</a> functions can be used to isolate
     * transformations within distinct drawing groups.
     *
     * Note: Transformations are reset at the beginning of the draw loop. Calling
     * `shearX(1)` inside the <a href="#/p5/draw">draw()</a> function won't
     * cause shapes to shear continuously.
     *
     * @method shearX
     * @param  {Number} angle angle to shear by in the current <a href="#/p5/angleMode">angleMode()</a>.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A white quadrilateral on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Shear the coordinate system along the x-axis.
     *   shearX(QUARTER_PI);
     *
     *   // Draw the square.
     *   square(0, 0, 50);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   describe('A white quadrilateral on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Shear the coordinate system along the x-axis.
     *   shearX(45);
     *
     *   // Draw the square.
     *   square(0, 0, 50);
     * }
     */
    fn.shearX = function(angle) {
      // p5._validateParameters('shearX', arguments);
      const rad = this._toRadians(angle);
      this._renderer.applyMatrix(1, 0, Math.tan(rad), 1, 0, 0);
      return this;
    };

    /**
     * Shears the y-axis so that shapes appear skewed.
     *
     * By default, the x- and y-axes are perpendicular. The `shearY()` function
     * transforms the coordinate system so that y-coordinates are translated while
     * x-coordinates are fixed.
     *
     * The first parameter, `angle`, is the amount to shear. For example, calling
     * `shearY(1)` transforms all y-coordinates using the formula
     * `y = y + x * tan(angle)`. `shearY()` interprets angle values using the
     * current <a href="#/p5/angleMode">angleMode()</a>.
     *
     * By default, transformations accumulate. For example, calling
     * `shearY(1)` twice has the same effect as calling `shearY(2)` once. The
     * <a href="#/p5/push">push()</a> and
     * <a href="#/p5/pop">pop()</a> functions can be used to isolate
     * transformations within distinct drawing groups.
     *
     * Note: Transformations are reset at the beginning of the draw loop. Calling
     * `shearY(1)` inside the <a href="#/p5/draw">draw()</a> function won't
     * cause shapes to shear continuously.
     *
     * @method shearY
     * @param  {Number} angle angle to shear by in the current <a href="#/p5/angleMode">angleMode()</a>.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A white quadrilateral on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Shear the coordinate system along the y-axis.
     *   shearY(QUARTER_PI);
     *
     *   // Draw the square.
     *   square(0, 0, 50);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   describe('A white quadrilateral on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Shear the coordinate system along the y-axis.
     *   shearY(45);
     *
     *   // Draw the square.
     *   square(0, 0, 50);
     * }
     */
    fn.shearY = function(angle) {
      // p5._validateParameters('shearY', arguments);
      const rad = this._toRadians(angle);
      this._renderer.applyMatrix(1, Math.tan(rad), 0, 1, 0, 0);
      return this;
    };

    /**
     * Translates the coordinate system.
     *
     * By default, the origin `(0, 0)` is at the sketch's top-left corner in 2D
     * mode and center in WebGL mode. The `translate()` function shifts the origin
     * to a different position. Everything drawn after `translate()` is called
     * will appear to be shifted. There are two ways to call `translate()` with
     * parameters that set the origin's position.
     *
     * The first way to call `translate()` uses numbers to set the amount of
     * translation. The first two parameters, `x` and `y`, set the amount to
     * translate along the positive x- and y-axes. For example, calling
     * `translate(20, 30)` translates the origin 20 pixels along the x-axis and 30
     * pixels along the y-axis. The third parameter, `z`, is optional. It sets the
     * amount to translate along the positive z-axis. For example, calling
     * `translate(20, 30, 40)` translates the origin 20 pixels along the x-axis,
     * 30 pixels along the y-axis, and 40 pixels along the z-axis.
     *
     * The second way to call `translate()` uses a
     * <a href="#/p5.Vector">p5.Vector</a> object to set the amount of
     * translation. For example, calling `translate(myVector)` uses the x-, y-,
     * and z-components of `myVector` to set the amount to translate along the x-,
     * y-, and z-axes. Doing so is the same as calling
     * `translate(myVector.x, myVector.y, myVector.z)`.
     *
     * By default, transformations accumulate. For example, calling
     * `translate(10, 0)` twice has the same effect as calling
     * `translate(20, 0)` once. The <a href="#/p5/push">push()</a> and
     * <a href="#/p5/pop">pop()</a> functions can be used to isolate
     * transformations within distinct drawing groups.
     *
     * Note: Transformations are reset at the beginning of the draw loop. Calling
     * `translate(10, 0)` inside the <a href="#/p5/draw">draw()</a> function won't
     * cause shapes to move continuously.
     *
     * @method translate
     * @param  {Number} x amount to translate along the positive x-axis.
     * @param  {Number} y amount to translate along the positive y-axis.
     * @param  {Number} [z] amount to translate along the positive z-axis.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A white circle on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Translate the origin to the center.
     *   translate(50, 50);
     *
     *   // Draw a circle at coordinates (0, 0).
     *   circle(0, 0, 40);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'Two circles drawn on a gray background. The blue circle on the right overlaps the red circle at the center.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Translate the origin to the center.
     *   translate(50, 50);
     *
     *   // Draw the red circle.
     *   fill('red');
     *   circle(0, 0, 40);
     *
     *   // Translate the origin to the right.
     *   translate(25, 0);
     *
     *   // Draw the blue circle.
     *   fill('blue');
     *   circle(0, 0, 40);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A white circle moves slowly from left to right on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the x-coordinate.
     *   let x = frameCount * 0.2;
     *
     *   // Translate the origin.
     *   translate(x, 50);
     *
     *   // Draw a circle at coordinates (0, 0).
     *   circle(0, 0, 40);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A white circle on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Create a p5.Vector object.
     *   let v = createVector(50, 50);
     *
     *   // Translate the origin by the vector.
     *   translate(v);
     *
     *   // Draw a circle at coordinates (0, 0).
     *   circle(0, 0, 40);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     'Two spheres sitting side-by-side on gray background. The sphere at the center is red. The sphere on the right is blue.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Style the spheres.
     *   noStroke();
     *
     *   // Draw the red sphere.
     *   fill('red');
     *   sphere(10);
     *
     *   // Translate the origin to the right.
     *   translate(30, 0, 0);
     *
     *   // Draw the blue sphere.
     *   fill('blue');
     *   sphere(10);
     * }
     */
    /**
     * @method translate
     * @param  {p5.Vector} vector vector by which to translate.
     * @chainable
     */
    fn.translate = function(x, y, z) {
      // p5._validateParameters('translate', arguments);
      if (this._renderer.isP3D) {
        this._renderer.translate(x, y, z);
      } else {
        this._renderer.translate(x, y);
      }
      return this;
    };

    /**
     * Begins a drawing group that contains its own styles and transformations.
     *
     * By default, styles such as <a href="#/p5/fill">fill()</a> and
     * transformations such as <a href="#/p5/rotate">rotate()</a> are applied to
     * all drawing that follows. The `push()` and <a href="#/p5/pop">pop()</a>
     * functions can limit the effect of styles and transformations to a specific
     * group of shapes, images, and text. For example, a group of shapes could be
     * translated to follow the mouse without affecting the rest of the sketch:
     *
     * ```js
     * // Begin the drawing group.
     * push();
     *
     * // Translate the origin to the mouse's position.
     * translate(mouseX, mouseY);
     *
     * // Style the face.
     * noStroke();
     * fill('green');
     *
     * // Draw the face.
     * circle(0, 0, 60);
     *
     * // Style the eyes.
     * fill('white');
     *
     * // Draw the left eye.
     * ellipse(-20, -20, 30, 20);
     *
     * // Draw the right eye.
     * ellipse(20, -20, 30, 20);
     *
     * // End the drawing group.
     * pop();
     *
     * // Draw a bug.
     * let x = random(0, 100);
     * let y = random(0, 100);
     * text('🦟', x, y);
     * ```
     *
     * In the code snippet above, the bug's position isn't affected by
     * `translate(mouseX, mouseY)` because that transformation is contained
     * between `push()` and <a href="#/p5/pop">pop()</a>. The bug moves around
     * the entire canvas as expected.
     *
     * Note: `push()` and <a href="#/p5/pop">pop()</a> are always called as a
     * pair. Both functions are required to begin and end a drawing group.
     *
     * `push()` and <a href="#/p5/pop">pop()</a> can also be nested to create
     * subgroups. For example, the code snippet above could be changed to give
     * more detail to the frog’s eyes:
     *
     * ```js
     * // Begin the drawing group.
     * push();
     *
     * // Translate the origin to the mouse's position.
     * translate(mouseX, mouseY);
     *
     * // Style the face.
     * noStroke();
     * fill('green');
     *
     * // Draw a face.
     * circle(0, 0, 60);
     *
     * // Style the eyes.
     * fill('white');
     *
     * // Draw the left eye.
     * push();
     * translate(-20, -20);
     * ellipse(0, 0, 30, 20);
     * fill('black');
     * circle(0, 0, 8);
     * pop();
     *
     * // Draw the right eye.
     * push();
     * translate(20, -20);
     * ellipse(0, 0, 30, 20);
     * fill('black');
     * circle(0, 0, 8);
     * pop();
     *
     * // End the drawing group.
     * pop();
     *
     * // Draw a bug.
     * let x = random(0, 100);
     * let y = random(0, 100);
     * text('🦟', x, y);
     * ```
     *
     * In this version, the code to draw each eye is contained between its own
     * `push()` and <a href="#/p5/pop">pop()</a> functions. Doing so makes it
     * easier to add details in the correct part of a drawing.
     *
     * `push()` and <a href="#/p5/pop">pop()</a> contain the effects of the
     * following functions:
     *
     * - <a href="#/p5/fill">fill()</a>
     * - <a href="#/p5/noFill">noFill()</a>
     * - <a href="#/p5/noStroke">noStroke()</a>
     * - <a href="#/p5/stroke">stroke()</a>
     * - <a href="#/p5/tint">tint()</a>
     * - <a href="#/p5/noTint">noTint()</a>
     * - <a href="#/p5/strokeWeight">strokeWeight()</a>
     * - <a href="#/p5/strokeCap">strokeCap()</a>
     * - <a href="#/p5/strokeJoin">strokeJoin()</a>
     * - <a href="#/p5/imageMode">imageMode()</a>
     * - <a href="#/p5/rectMode">rectMode()</a>
     * - <a href="#/p5/ellipseMode">ellipseMode()</a>
     * - <a href="#/p5/colorMode">colorMode()</a>
     * - <a href="#/p5/textAlign">textAlign()</a>
     * - <a href="#/p5/textFont">textFont()</a>
     * - <a href="#/p5/textSize">textSize()</a>
     * - <a href="#/p5/textLeading">textLeading()</a>
     * - <a href="#/p5/applyMatrix">applyMatrix()</a>
     * - <a href="#/p5/resetMatrix">resetMatrix()</a>
     * - <a href="#/p5/rotate">rotate()</a>
     * - <a href="#/p5/scale">scale()</a>
     * - <a href="#/p5/shearX">shearX()</a>
     * - <a href="#/p5/shearY">shearY()</a>
     * - <a href="#/p5/translate">translate()</a>
     *
     * In WebGL mode, `push()` and <a href="#/p5/pop">pop()</a> contain the
     * effects of a few additional styles:
     *
     * - <a href="#/p5/setCamera">setCamera()</a>
     * - <a href="#/p5/ambientLight">ambientLight()</a>
     * - <a href="#/p5/directionalLight">directionalLight()</a>
     * - <a href="#/p5/pointLight">pointLight()</a> <a href="#/p5/texture">texture()</a>
     * - <a href="#/p5/specularMaterial">specularMaterial()</a>
     * - <a href="#/p5/shininess">shininess()</a>
     * - <a href="#/p5/normalMaterial">normalMaterial()</a>
     * - <a href="#/p5/shader">shader()</a>
     *
     * @method push
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Draw the left circle.
     *   circle(25, 50, 20);
     *
     *   // Begin the drawing group.
     *   push();
     *
     *   // Translate the origin to the center.
     *   translate(50, 50);
     *
     *   // Style the circle.
     *   strokeWeight(5);
     *   stroke('royalblue');
     *   fill('orange');
     *
     *   // Draw the circle.
     *   circle(0, 0, 20);
     *
     *   // End the drawing group.
     *   pop();
     *
     *   // Draw the right circle.
     *   circle(75, 50, 20);
     *
     *   describe(
     *     'Three circles drawn in a row on a gray background. The left and right circles are white with thin, black borders. The middle circle is orange with a thick, blue border.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Slow the frame rate.
     *   frameRate(24);
     *
     *   describe('A mosquito buzzes in front of a green frog. The frog follows the mouse as the user moves.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Begin the drawing group.
     *   push();
     *
     *   // Translate the origin to the mouse's position.
     *   translate(mouseX, mouseY);
     *
     *   // Style the face.
     *   noStroke();
     *   fill('green');
     *
     *   // Draw a face.
     *   circle(0, 0, 60);
     *
     *   // Style the eyes.
     *   fill('white');
     *
     *   // Draw the left eye.
     *   push();
     *   translate(-20, -20);
     *   ellipse(0, 0, 30, 20);
     *   fill('black');
     *   circle(0, 0, 8);
     *   pop();
     *
     *   // Draw the right eye.
     *   push();
     *   translate(20, -20);
     *   ellipse(0, 0, 30, 20);
     *   fill('black');
     *   circle(0, 0, 8);
     *   pop();
     *
     *   // End the drawing group.
     *   pop();
     *
     *   // Draw a bug.
     *   let x = random(0, 100);
     *   let y = random(0, 100);
     *   text('🦟', x, y);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     'Two spheres drawn on a gray background. The sphere on the left is red and lit from the front. The sphere on the right is a blue wireframe.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the red sphere.
     *   push();
     *   translate(-25, 0, 0);
     *   noStroke();
     *   directionalLight(255, 0, 0, 0, 0, -1);
     *   sphere(20);
     *   pop();
     *
     *   // Draw the blue sphere.
     *   push();
     *   translate(25, 0, 0);
     *   strokeWeight(0.3);
     *   stroke(0, 0, 255);
     *   noFill();
     *   sphere(20);
     *   pop();
     * }
     */
    fn.push = function() {
      this._renderer.push();
    };

    /**
     * Ends a drawing group that contains its own styles and transformations.
     *
     * By default, styles such as <a href="#/p5/fill">fill()</a> and
     * transformations such as <a href="#/p5/rotate">rotate()</a> are applied to
     * all drawing that follows. The <a href="#/p5/push">push()</a> and `pop()`
     * functions can limit the effect of styles and transformations to a specific
     * group of shapes, images, and text. For example, a group of shapes could be
     * translated to follow the mouse without affecting the rest of the sketch:
     *
     * ```js
     * // Begin the drawing group.
     * push();
     *
     * // Translate the origin to the mouse's position.
     * translate(mouseX, mouseY);
     *
     * // Style the face.
     * noStroke();
     * fill('green');
     *
     * // Draw the face.
     * circle(0, 0, 60);
     *
     * // Style the eyes.
     * fill('white');
     *
     * // Draw the left eye.
     * ellipse(-20, -20, 30, 20);
     *
     * // Draw the right eye.
     * ellipse(20, -20, 30, 20);
     *
     * // End the drawing group.
     * pop();
     *
     * // Draw a bug.
     * let x = random(0, 100);
     * let y = random(0, 100);
     * text('🦟', x, y);
     * ```
     *
     * In the code snippet above, the bug's position isn't affected by
     * `translate(mouseX, mouseY)` because that transformation is contained
     * between <a href="#/p5/push">push()</a> and `pop()`. The bug moves around
     * the entire canvas as expected.
     *
     * Note: <a href="#/p5/push">push()</a> and `pop()` are always called as a
     * pair. Both functions are required to begin and end a drawing group.
     *
     * <a href="#/p5/push">push()</a> and `pop()` can also be nested to create
     * subgroups. For example, the code snippet above could be changed to give
     * more detail to the frog’s eyes:
     *
     * ```js
     * // Begin the drawing group.
     * push();
     *
     * // Translate the origin to the mouse's position.
     * translate(mouseX, mouseY);
     *
     * // Style the face.
     * noStroke();
     * fill('green');
     *
     * // Draw a face.
     * circle(0, 0, 60);
     *
     * // Style the eyes.
     * fill('white');
     *
     * // Draw the left eye.
     * push();
     * translate(-20, -20);
     * ellipse(0, 0, 30, 20);
     * fill('black');
     * circle(0, 0, 8);
     * pop();
     *
     * // Draw the right eye.
     * push();
     * translate(20, -20);
     * ellipse(0, 0, 30, 20);
     * fill('black');
     * circle(0, 0, 8);
     * pop();
     *
     * // End the drawing group.
     * pop();
     *
     * // Draw a bug.
     * let x = random(0, 100);
     * let y = random(0, 100);
     * text('🦟', x, y);
     * ```
     *
     * In this version, the code to draw each eye is contained between its own
     * <a href="#/p5/push">push()</a> and `pop()` functions. Doing so makes it
     * easier to add details in the correct part of a drawing.
     *
     * <a href="#/p5/push">push()</a> and `pop()` contain the effects of the
     * following functions:
     *
     * - <a href="#/p5/fill">fill()</a>
     * - <a href="#/p5/noFill">noFill()</a>
     * - <a href="#/p5/noStroke">noStroke()</a>
     * - <a href="#/p5/stroke">stroke()</a>
     * - <a href="#/p5/tint">tint()</a>
     * - <a href="#/p5/noTint">noTint()</a>
     * - <a href="#/p5/strokeWeight">strokeWeight()</a>
     * - <a href="#/p5/strokeCap">strokeCap()</a>
     * - <a href="#/p5/strokeJoin">strokeJoin()</a>
     * - <a href="#/p5/imageMode">imageMode()</a>
     * - <a href="#/p5/rectMode">rectMode()</a>
     * - <a href="#/p5/ellipseMode">ellipseMode()</a>
     * - <a href="#/p5/colorMode">colorMode()</a>
     * - <a href="#/p5/textAlign">textAlign()</a>
     * - <a href="#/p5/textFont">textFont()</a>
     * - <a href="#/p5/textSize">textSize()</a>
     * - <a href="#/p5/textLeading">textLeading()</a>
     * - <a href="#/p5/applyMatrix">applyMatrix()</a>
     * - <a href="#/p5/resetMatrix">resetMatrix()</a>
     * - <a href="#/p5/rotate">rotate()</a>
     * - <a href="#/p5/scale">scale()</a>
     * - <a href="#/p5/shearX">shearX()</a>
     * - <a href="#/p5/shearY">shearY()</a>
     * - <a href="#/p5/translate">translate()</a>
     *
     * In WebGL mode, <a href="#/p5/push">push()</a> and `pop()` contain the
     * effects of a few additional styles:
     *
     * - <a href="#/p5/setCamera">setCamera()</a>
     * - <a href="#/p5/ambientLight">ambientLight()</a>
     * - <a href="#/p5/directionalLight">directionalLight()</a>
     * - <a href="#/p5/pointLight">pointLight()</a> <a href="#/p5/texture">texture()</a>
     * - <a href="#/p5/specularMaterial">specularMaterial()</a>
     * - <a href="#/p5/shininess">shininess()</a>
     * - <a href="#/p5/normalMaterial">normalMaterial()</a>
     * - <a href="#/p5/shader">shader()</a>
     *
     * @method pop
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Draw the left circle.
     *   circle(25, 50, 20);
     *
     *   // Begin the drawing group.
     *   push();
     *
     *   // Translate the origin to the center.
     *   translate(50, 50);
     *
     *   // Style the circle.
     *   strokeWeight(5);
     *   stroke('royalblue');
     *   fill('orange');
     *
     *   // Draw the circle.
     *   circle(0, 0, 20);
     *
     *   // End the drawing group.
     *   pop();
     *
     *   // Draw the right circle.
     *   circle(75, 50, 20);
     *
     *   describe(
     *     'Three circles drawn in a row on a gray background. The left and right circles are white with thin, black borders. The middle circle is orange with a thick, blue border.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Slow the frame rate.
     *   frameRate(24);
     *
     *   describe('A mosquito buzzes in front of a green frog. The frog follows the mouse as the user moves.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Begin the drawing group.
     *   push();
     *
     *   // Translate the origin to the mouse's position.
     *   translate(mouseX, mouseY);
     *
     *   // Style the face.
     *   noStroke();
     *   fill('green');
     *
     *   // Draw a face.
     *   circle(0, 0, 60);
     *
     *   // Style the eyes.
     *   fill('white');
     *
     *   // Draw the left eye.
     *   push();
     *   translate(-20, -20);
     *   ellipse(0, 0, 30, 20);
     *   fill('black');
     *   circle(0, 0, 8);
     *   pop();
     *
     *   // Draw the right eye.
     *   push();
     *   translate(20, -20);
     *   ellipse(0, 0, 30, 20);
     *   fill('black');
     *   circle(0, 0, 8);
     *   pop();
     *
     *   // End the drawing group.
     *   pop();
     *
     *   // Draw a bug.
     *   let x = random(0, 100);
     *   let y = random(0, 100);
     *   text('🦟', x, y);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     'Two spheres drawn on a gray background. The sphere on the left is red and lit from the front. The sphere on the right is a blue wireframe.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the red sphere.
     *   push();
     *   translate(-25, 0, 0);
     *   noStroke();
     *   directionalLight(255, 0, 0, 0, 0, -1);
     *   sphere(20);
     *   pop();
     *
     *   // Draw the blue sphere.
     *   push();
     *   translate(25, 0, 0);
     *   strokeWeight(0.3);
     *   stroke(0, 0, 255);
     *   noFill();
     *   sphere(20);
     *   pop();
     * }
     */
    fn.pop = function() {
      this._renderer.pop();
    };
  }

  if(typeof p5 !== 'undefined'){
    transform$1(p5, p5.prototype);
  }

  /**
   * @module Structure
   * @submodule Structure
   * @for p5
   * @requires core
   */

  function structure(p5, fn){
    /**
     * Stops the code in <a href="#/p5/draw">draw()</a> from running repeatedly.
     *
     * By default, <a href="#/p5/draw">draw()</a> tries to run 60 times per
     * second. Calling `noLoop()` stops <a href="#/p5/draw">draw()</a> from
     * repeating. The draw loop can be restarted by calling
     * <a href="#/p5/loop">loop()</a>. <a href="#/p5/draw">draw()</a> can be run
     * once by calling <a href="#/p5/redraw">redraw()</a>.
     *
     * The <a href="#/p5/isLooping">isLooping()</a> function can be used to check
     * whether a sketch is looping, as in `isLooping() === true`.
     *
     * @method noLoop
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Turn off the draw loop.
     *   noLoop();
     *
     *   describe('A white half-circle on the left edge of a gray square.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the circle's x-coordinate.
     *   let x = frameCount;
     *
     *   // Draw the circle.
     *   // Normally, the circle would move from left to right.
     *   circle(x, 50, 20);
     * }
     *
     * @example
     * // Double-click to stop the draw loop.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Slow the frame rate.
     *   frameRate(5);
     *
     *   describe('A white circle moves randomly on a gray background. It stops moving when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the circle's coordinates.
     *   let x = random(0, 100);
     *   let y = random(0, 100);
     *
     *   // Draw the circle.
     *   // Normally, the circle would move from left to right.
     *   circle(x, y, 20);
     * }
     *
     * // Stop the draw loop when the user double-clicks.
     * function doubleClicked() {
     *   noLoop();
     * }
     *
     * @example
     * let startButton;
     * let stopButton;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create the button elements and place them
     *   // beneath the canvas.
     *   startButton = createButton('▶');
     *   startButton.position(0, 100);
     *   startButton.size(50, 20);
     *   stopButton = createButton('◾');
     *   stopButton.position(50, 100);
     *   stopButton.size(50, 20);
     *
     *   // Set functions to call when the buttons are pressed.
     *   startButton.mousePressed(loop);
     *   stopButton.mousePressed(noLoop);
     *
     *   // Slow the frame rate.
     *   frameRate(5);
     *
     *   describe(
     *     'A white circle moves randomly on a gray background. Play and stop buttons are shown beneath the canvas. The circle stops or starts moving when the user presses a button.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the circle's coordinates.
     *   let x = random(0, 100);
     *   let y = random(0, 100);
     *
     *   // Draw the circle.
     *   // Normally, the circle would move from left to right.
     *   circle(x, y, 20);
     * }
     */
    fn.noLoop = function() {
      this._loop = false;
    };

    /**
     * Resumes the draw loop after <a href="#/p5/noLoop">noLoop()</a> has been
     * called.
     *
     * By default, <a href="#/p5/draw">draw()</a> tries to run 60 times per
     * second. Calling <a href="#/p5/noLoop">noLoop()</a> stops
     * <a href="#/p5/draw">draw()</a> from repeating. The draw loop can be
     * restarted by calling `loop()`.
     *
     * The <a href="#/p5/isLooping">isLooping()</a> function can be used to check
     * whether a sketch is looping, as in `isLooping() === true`.
     *
     * @method loop
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Turn off the draw loop.
     *   noLoop();
     *
     *   describe(
     *     'A white half-circle on the left edge of a gray square. The circle starts moving to the right when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the circle's x-coordinate.
     *   let x = frameCount;
     *
     *   // Draw the circle.
     *   circle(x, 50, 20);
     * }
     *
     * // Resume the draw loop when the user double-clicks.
     * function doubleClicked() {
     *   loop();
     * }
     *
     * @example
     * let startButton;
     * let stopButton;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create the button elements and place them
     *   // beneath the canvas.
     *   startButton = createButton('▶');
     *   startButton.position(0, 100);
     *   startButton.size(50, 20);
     *   stopButton = createButton('◾');
     *   stopButton.position(50, 100);
     *   stopButton.size(50, 20);
     *
     *   // Set functions to call when the buttons are pressed.
     *   startButton.mousePressed(loop);
     *   stopButton.mousePressed(noLoop);
     *
     *   // Slow the frame rate.
     *   frameRate(5);
     *
     *   describe(
     *     'A white circle moves randomly on a gray background. Play and stop buttons are shown beneath the canvas. The circle stops or starts moving when the user presses a button.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the circle's coordinates.
     *   let x = random(0, 100);
     *   let y = random(0, 100);
     *
     *   // Draw the circle.
     *   // Normally, the circle would move from left to right.
     *   circle(x, y, 20);
     * }
     */
    fn.loop = function() {
      if (!this._loop) {
        this._loop = true;
        if (this._setupDone) {
          this._draw();
        }
      }
    };

    /**
     * Returns `true` if the draw loop is running and `false` if not.
     *
     * By default, <a href="#/p5/draw">draw()</a> tries to run 60 times per
     * second. Calling <a href="#/p5/noLoop">noLoop()</a> stops
     * <a href="#/p5/draw">draw()</a> from repeating. The draw loop can be
     * restarted by calling <a href="#/p5/loop">loop()</a>.
     *
     * The `isLooping()` function can be used to check whether a sketch is
     * looping, as in `isLooping() === true`.
     *
     * @method isLooping
     * @returns {boolean}
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A white circle drawn against a gray background. When the user double-clicks, the circle stops or resumes following the mouse.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw the circle at the mouse's position.
     *   circle(mouseX, mouseY, 20);
     * }
     *
     * // Toggle the draw loop when the user double-clicks.
     * function doubleClicked() {
     *   if (isLooping() === true) {
     *     noLoop();
     *   } else {
     *     loop();
     *   }
     * }
     */
    fn.isLooping = function() {
      return this._loop;
    };

    /**
     * Runs the code in <a href="#/p5/draw">draw()</a> once.
     *
     * By default, <a href="#/p5/draw">draw()</a> tries to run 60 times per
     * second. Calling <a href="#/p5/noLoop">noLoop()</a> stops
     * <a href="#/p5/draw">draw()</a> from repeating. Calling `redraw()` will
     * execute the code in the <a href="#/p5/draw">draw()</a> function a set
     * number of times. `await` the result of `redraw` to make sure it has finished.
     *
     * The parameter, `n`, is optional. If a number is passed, as in `redraw(5)`,
     * then the draw loop will run the given number of times. By default, `n` is
     * 1.
     *
     * @method redraw
     * @param  {Integer} [n] number of times to run <a href="#/p5/draw">draw()</a>. Defaults to 1.
     * @returns {Promise<void>}
     *
     * @example
     * // Double-click the canvas to move the circle.
     *
     * let x = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Turn off the draw loop.
     *   noLoop();
     *
     *   describe(
     *     'A white half-circle on the left edge of a gray square. The circle moves a little to the right when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw the circle.
     *   circle(x, 50, 20);
     *
     *   // Increment x.
     *   x += 5;
     * }
     *
     * // Run the draw loop when the user double-clicks.
     * async function doubleClicked() {
     *   await redraw();
     * }
     *
     * @example
     * // Double-click the canvas to move the circle.
     *
     * let x = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Turn off the draw loop.
     *   noLoop();
     *
     *   describe(
     *     'A white half-circle on the left edge of a gray square. The circle hops to the right when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw the circle.
     *   circle(x, 50, 20);
     *
     *   // Increment x.
     *   x += 5;
     * }
     *
     * // Run the draw loop three times when the user double-clicks.
     * async function doubleClicked() {
     *   await redraw(3);
     * }
     */
    fn.redraw = async function(n) {
      if (this._inUserDraw || !this._setupDone) {
        return;
      }

      let numberOfRedraws = parseInt(n);
      if (isNaN(numberOfRedraws) || numberOfRedraws < 1) {
        numberOfRedraws = 1;
      }

      const context = this._isGlobal ? window : this;
      if (typeof context.draw === 'function') {
        if (typeof context.setup === 'undefined') {
          context.scale(context._pixelDensity, context._pixelDensity);
        }
        for (let idxRedraw = 0; idxRedraw < numberOfRedraws; idxRedraw++) {
          context.resetMatrix();
          if (this._accessibleOutputs.grid || this._accessibleOutputs.text) {
            this._updateAccsOutput();
          }
          if (this._renderer.isP3D) {
            this._renderer._update();
          }
          this.frameCount = context.frameCount + 1;
          await this._runLifecycleHook('predraw');
          this._inUserDraw = true;
          try {
            await context.draw();
          } finally {
            this._inUserDraw = false;
          }
          await this._runLifecycleHook('postdraw');
        }
        // Finish drawing
        await this._renderer.finishDraw?.();
      }
    };

    /**
     * Creates a new sketch in "instance" mode.
     *
     * All p5.js sketches are instances of the `p5` class. Put another way, all
     * p5.js sketches are objects with methods including `pInst.setup()`,
     * `pInst.draw()`, `pInst.circle()`, and `pInst.fill()`. By default, sketches
     * run in "global mode" to hide some of this complexity.
     *
     * In global mode, a default instance of the `p5` class is created
     * automatically. The default `p5` instance searches the web page's source
     * code for declarations of system functions such as `setup()`, `draw()`,
     * and `mousePressed()`, then attaches those functions to itself as methods.
     * Calling a function such as `circle()` in global mode actually calls the
     * default `p5` object's `pInst.circle()` method.
     *
     * It's often helpful to isolate the code within sketches from the rest of the
     * code on a web page. Two common use cases are web pages that use other
     * JavaScript libraries and web pages with multiple sketches. "Instance mode"
     * makes it easy to support both of these scenarios.
     *
     * Instance mode sketches support the same API as global mode sketches. They
     * use a function to bundle, or encapsulate, an entire sketch. The function
     * containing the sketch is then passed to the `p5()` constructor.
     *
     * The first parameter, `sketch`, is a function that contains the sketch. For
     * example, the statement `new p5(mySketch)` would create a new instance mode
     * sketch from a function named `mySketch`. The function should have one
     * parameter, `p`, that's a `p5` object.
     *
     * The second parameter, `node`, is optional. If a string is passed, as in
     * `new p5(mySketch, 'sketch-one')` the new instance mode sketch will become a
     * child of the HTML element with the id `sketch-one`. If an HTML element is
     * passed, as in `new p5(mySketch, myElement)`, then the new instance mode
     * sketch will become a child of the `Element` object called `myElement`.
     *
     * @method p5
     * @param {Object} sketch function containing the sketch.
     * @param {String|HTMLElement} node ID or reference to the HTML element that will contain the sketch.
     *
     * @example
     * // META:norender
     * // Declare the function containing the sketch.
     * function sketch(p) {
     *
     *   // Declare the setup() method.
     *   p.setup = function () {
     *     p.createCanvas(100, 100);
     *
     *     p.describe('A white circle drawn on a gray background.');
     *   };
     *
     *   // Declare the draw() method.
     *   p.draw = function () {
     *     p.background(200);
     *
     *     // Draw the circle.
     *     p.circle(50, 50, 20);
     *   };
     * }
     *
     * // Initialize the sketch.
     * new p5(sketch);
     *
     * @example
     * // META:norender
     * // Declare the function containing the sketch.
     * function sketch(p) {
     *   // Create the sketch's variables within its scope.
     *   let x = 50;
     *   let y = 50;
     *
     *   // Declare the setup() method.
     *   p.setup = function () {
     *     p.createCanvas(100, 100);
     *
     *     p.describe('A white circle moves randomly on a gray background.');
     *   };
     *
     *   // Declare the draw() method.
     *   p.draw = function () {
     *     p.background(200);
     *
     *     // Update x and y.
     *     x += p.random(-1, 1);
     *     y += p.random(-1, 1);
     *
     *     // Draw the circle.
     *     p.circle(x, y, 20);
     *   };
     * }
     *
     * // Initialize the sketch.
     * new p5(sketch);
     *
     * @example
     * // META:norender
     * // Declare the function containing the sketch.
     * function sketch(p) {
     *
     *   // Declare the setup() method.
     *   p.setup = function () {
     *     p.createCanvas(100, 100);
     *
     *     p.describe('A white circle drawn on a gray background.');
     *   };
     *
     *   // Declare the draw() method.
     *   p.draw = function () {
     *     p.background(200);
     *
     *     // Draw the circle.
     *     p.circle(50, 50, 20);
     *   };
     * }
     *
     * // Select the web page's body element.
     * let body = document.querySelector('body');
     *
     * // Initialize the sketch and attach it to the web page's body.
     * new p5(sketch, body);
     *
     * @example
     * // META:norender
     * // Declare the function containing the sketch.
     * function sketch(p) {
     *
     *   // Declare the setup() method.
     *   p.setup = function () {
     *     p.createCanvas(100, 100);
     *
     *     p.describe(
     *       'A white circle drawn on a gray background. The circle follows the mouse as the user moves.'
     *     );
     *   };
     *
     *   // Declare the draw() method.
     *   p.draw = function () {
     *     p.background(200);
     *
     *     // Draw the circle.
     *     p.circle(p.mouseX, p.mouseY, 20);
     *   };
     * }
     *
     * // Initialize the sketch.
     * new p5(sketch);
     *
     * @example
     * // META:norender
     * // Declare the function containing the sketch.
     * function sketch(p) {
     *
     *   // Declare the setup() method.
     *   p.setup = function () {
     *     p.createCanvas(100, 100);
     *
     *     p.describe(
     *       'A white circle drawn on a gray background. The circle follows the mouse as the user moves. The circle becomes black when the user double-clicks.'
     *     );
     *   };
     *
     *   // Declare the draw() method.
     *   p.draw = function () {
     *     p.background(200);
     *
     *     // Draw the circle.
     *     p.circle(p.mouseX, p.mouseY, 20);
     *   };
     *
     *   // Declare the doubleClicked() method.
     *   p.doubleClicked = function () {
     *     // Change the fill color when the user double-clicks.
     *     p.fill(0);
     *   };
     * }
     *
     * // Initialize the sketch.
     * new p5(sketch);
     */
  }

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

  /**
   * @module Environment
   * @submodule Environment
   * @for p5
   * @requires core
   * @requires constants
   */

  // import { Vector } from '../math/p5.Vector';

  function environment$1(p5, fn, lifecycles){
    const standardCursors = [ARROW, CROSS, HAND, MOVE, TEXT, WAIT];

    fn._frameRate = 0;
    fn._lastFrameTime = window.performance.now();
    fn._targetFrameRate = 60;

    const _windowPrint = window.print;
    let windowPrintDisabled = false;

    lifecycles.presetup = function(){
      const events = [
        'resize'
      ];

      for(const event of events){
        window.addEventListener(event, this[`_on${event}`].bind(this), {
          passive: false,
          signal: this._removeSignal
        });
      }
    };

    /**
     * Displays text in the web browser's console.
     *
     * `print()` is helpful for printing values while debugging. Each call to
     * `print()` creates a new line of text.
     *
     * Note: Call `print('\n')` to print a blank line. Calling `print()` without
     * an argument opens the browser's dialog for printing documents.
     *
     * @method print
     * @param {Any} contents content to print to the console.
     * @example
     * // META:norender
     * function setup() {
     *   // Prints "hello, world" to the console.
     *   print('hello, world');
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   let name = 'ada';
     *   // Prints "hello, ada" to the console.
     *   print(`hello, ${name}`);
     * }
     */
    fn.print = function(...args) {
      if (!args.length) {
        if (!windowPrintDisabled) {
          _windowPrint();
          if (
            window.confirm(
              'You just tried to print the webpage. Do you want to prevent this from running again?'
            )
          ) {
            windowPrintDisabled = true;
          }
        }
      } else {
        console.log(...args);
      }
    };

    /**
     * A `Number` variable that tracks the number of frames drawn since the sketch
     * started.
     *
     * `frameCount`'s value is 0 inside <a href="#/p5/setup">setup()</a>. It
     * increments by 1 each time the code in <a href="#/p5/draw">draw()</a>
     * finishes executing.
     *
     * @property {Integer} frameCount
     * @readOnly
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Display the value of
     *   // frameCount.
     *   textSize(30);
     *   textAlign(CENTER, CENTER);
     *   text(frameCount, 50, 50);
     *
     *   describe('The number 0 written in black in the middle of a gray square.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Set the frameRate to 30.
     *   frameRate(30);
     *
     *   textSize(30);
     *   textAlign(CENTER, CENTER);
     *
     *   describe('A number written in black in the middle of a gray square. Its value increases rapidly.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Display the value of
     *   // frameCount.
     *   text(frameCount, 50, 50);
     * }
     */
    fn.frameCount = 0;

    /**
     * A `Number` variable that tracks the number of milliseconds it took to draw
     * the last frame.
     *
     * `deltaTime` contains the amount of time it took
     * <a href="#/p5/draw">draw()</a> to execute during the previous frame. It's
     * useful for simulating physics.
     *
     * @property {Integer} deltaTime
     * @readOnly
     * @example
     * let x = 0;
     * let speed = 0.05;
     *
     * function setup()  {
     *   createCanvas(100, 100);
     *
     *   // Set the frameRate to 30.
     *   frameRate(30);
     *
     *   describe('A white circle moves from left to right on a gray background. It reappears on the left side when it reaches the right side.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Use deltaTime to calculate
     *   // a change in position.
     *   let deltaX = speed * deltaTime;
     *
     *   // Update the x variable.
     *   x += deltaX;
     *
     *   // Reset x to 0 if it's
     *   // greater than 100.
     *   if (x > 100)  {
     *     x = 0;
     *   }
     *
     *   // Use x to set the circle's
     *   // position.
     *   circle(x, 50, 20);
     * }
     */
    fn.deltaTime = 0;

    /**
     * A `Boolean` variable that's `true` if the browser is focused and `false` if
     * not.
     *
     * Note: The browser window can only receive input if it's focused.
     *
     * @property {Boolean} focused
     * @readOnly
     * @example
     * // Open this example in two separate browser
     * // windows placed side-by-side to demonstrate.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A square changes color from green to red when the browser window is out of focus.');
     * }
     *
     * function draw() {
     *   // Change the background color
     *   // when the browser window
     *   // goes in/out of focus.
     *   if (focused === true) {
     *     background(0, 255, 0);
     *   } else {
     *     background(255, 0, 0);
     *   }
     * }
     */
    fn.focused = document.hasFocus();

    /**
     * Changes the cursor's appearance.
     *
     * The first parameter, `type`, sets the type of cursor to display. The
     * built-in options are `ARROW`, `CROSS`, `HAND`, `MOVE`, `TEXT`, and `WAIT`.
     * `cursor()` also recognizes standard CSS cursor properties passed as
     * strings: `'help'`, `'wait'`, `'crosshair'`, `'not-allowed'`, `'zoom-in'`,
     * and `'grab'`. If the path to an image is passed, as in
     * `cursor('assets/target.png')`, then the image will be used as the cursor.
     * Images must be in .cur, .gif, .jpg, .jpeg, or .png format and should be <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#icon_size_limits">at most 32 by 32 pixels large.</a>
     *
     * The parameters `x` and `y` are optional. If an image is used for the
     * cursor, `x` and `y` set the location pointed to within the image. They are
     * both 0 by default, so the cursor points to the image's top-left corner. `x`
     * and `y` must be less than the image's width and height, respectively.
     *
     * @method cursor
     * @param {(ARROW|CROSS|HAND|MOVE|TEXT|WAIT|String)} type Built-in: either ARROW, CROSS, HAND, MOVE, TEXT, or WAIT.
     *                               Native CSS properties: 'grab', 'progress', and so on.
     *                               Path to cursor image.
     * @param {Number}          [x]  horizontal active spot of the cursor.
     * @param {Number}          [y]  vertical active spot of the cursor.
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A gray square. The cursor appears as crosshairs.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Set the cursor to crosshairs: +
     *   cursor(CROSS);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A gray square divided into quadrants. The cursor image changes when the mouse moves to each quadrant.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Divide the canvas into quadrants.
     *   line(50, 0, 50, 100);
     *   line(0, 50, 100, 50);
     *
     *   // Change cursor based on mouse position.
     *   if (mouseX < 50 && mouseY < 50) {
     *     cursor(CROSS);
     *   } else if (mouseX > 50 && mouseY < 50) {
     *     cursor('progress');
     *   } else if (mouseX > 50 && mouseY > 50) {
     *     cursor('https://avatars0.githubusercontent.com/u/1617169?s=16');
     *   } else {
     *     cursor('grab');
     *   }
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('An image of three purple curves follows the mouse. The image shifts when the mouse is pressed.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Change the cursor's active spot
     *   // when the mouse is pressed.
     *   if (mouseIsPressed === true) {
     *     cursor('https://avatars0.githubusercontent.com/u/1617169?s=16', 8, 8);
     *   } else {
     *     cursor('https://avatars0.githubusercontent.com/u/1617169?s=16');
     *   }
     * }
     */
    fn.cursor = function(type, x, y) {
      let cursor = 'auto';
      const canvas = this._curElement.elt;
      if (standardCursors.includes(type)) {
        // Standard css cursor
        cursor = type;
      } else if (typeof type === 'string') {
        let coords = '';
        if (typeof x === 'number') { // fix for #8323
          y = typeof y === 'number' ? y : 0;
          // Note that x and y values must be unit-less positive integers < 32
          // https://developer.mozilla.org/en-US/docs/Web/CSS/cursor
          coords = `${Math.max(x, 0)} ${Math.max(y, 0)}`;
        }
        if (
          type.substring(0, 7) === 'http://' ||
          type.substring(0, 8) === 'https://'
        ) {
          // Image (absolute url)
          cursor = `url(${type}) ${coords}, auto`;
        } else if (/\.(cur|jpg|jpeg|gif|png|CUR|JPG|JPEG|GIF|PNG)$/.test(type)) {
          // Image file (relative path) - Separated for performance reasons
          cursor = `url(${type}) ${coords}, auto`;
        } else {
          // Any valid string for the css cursor property
          cursor = type;
        }
      }
      canvas.style.cursor = cursor;
    };

    /**
     * Sets the number of frames to draw per second.
     *
     * Calling `frameRate()` with one numeric argument, as in `frameRate(30)`,
     * attempts to draw 30 frames per second (FPS). The target frame rate may not
     * be achieved depending on the sketch's processing needs. Most computers
     * default to a frame rate of 60 FPS. Frame rates of 24 FPS and above are
     * fast enough for smooth animations.
     *
     * Calling `frameRate()` without an argument returns the current frame rate.
     * The value returned is an approximation.
     *
     * @method frameRate
     * @param  {Number} fps number of frames to draw per second.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A white circle on a gray background. The circle moves from left to right in a loop. It slows down when the mouse is pressed.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Set the x variable based
     *   // on the current frameCount.
     *   let x = frameCount % 100;
     *
     *   // If the mouse is pressed,
     *   // decrease the frame rate.
     *   if (mouseIsPressed === true) {
     *     frameRate(10);
     *   } else {
     *     frameRate(60);
     *   }
     *
     *   // Use x to set the circle's
     *   // position.
     *   circle(x, 50, 20);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A number written in black on a gray background. The number decreases when the mouse is pressed.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // If the mouse is pressed, do lots
     *   // of math to slow down drawing.
     *   if (mouseIsPressed === true) {
     *     for (let i = 0; i < 1000000; i += 1) {
     *       random();
     *     }
     *   }
     *
     *   // Get the current frame rate
     *   // and display it.
     *   let fps = frameRate();
     *   text(fps, 50, 50);
     * }
     */
    /**
     * @method frameRate
     * @return {Number}       current frame rate.
     */
    fn.frameRate = function(fps) {
      // p5._validateParameters('frameRate', arguments);
      if (typeof fps !== 'number' || fps < 0) {
        return this._frameRate;
      } else {
        this._targetFrameRate = fps;
        if (fps === 0) {
          this._frameRate = fps;
        }
        return this;
      }
    };

    /**
     * Returns the current framerate.
     *
     * @private
     * @return {Number} current frameRate
     */
    fn.getFrameRate = function() {
      return this.frameRate();
    };

    /**
     * Specifies the number of frames to be displayed every second. For example,
     * the function call frameRate(30) will attempt to refresh 30 times a second.
     * If the processor is not fast enough to maintain the specified rate, the
     * frame rate will not be achieved. Setting the frame rate within <a href="#/p5/setup">setup()</a> is
     * recommended. The default rate is 60 frames per second.
     *
     * Calling `frameRate()` with no arguments returns the current frame rate.
     *
     * @private
     * @param {Number} [fps] number of frames to be displayed every second
     */
    fn.setFrameRate = function(fps) {
      return this.frameRate(fps);
    };

    /**
     * Returns the target frame rate.
     *
     * The value is either the system frame rate or the last value passed to
     * <a href="#/p5/frameRate">frameRate()</a>.
     *
     * @method getTargetFrameRate
     * @return {Number} _targetFrameRate
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('The number 20 written in black on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Set the frame rate to 20.
     *   frameRate(20);
     *
     *   // Get the target frame rate and
     *   // display it.
     *   let fps = getTargetFrameRate();
     *   text(fps, 43, 54);
     * }
     */
    fn.getTargetFrameRate = function() {
      return this._targetFrameRate;
    };

    /**
     * Hides the cursor from view.
     *
     * @method noCursor
     * @example
     * function setup() {
     *   // Hide the cursor.
     *   noCursor();
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   circle(mouseX, mouseY, 10);
     *
     *   describe('A white circle on a gray background. The circle follows the mouse as it moves. The cursor is hidden.');
     * }
     */
    fn.noCursor = function() {
      this._curElement.elt.style.cursor = 'none';
    };

    /**
     * A `String` variable with the WebGL version in use.
     *
     * `webglVersion`'s value equals one of the following string constants:
     *
     * - `WEBGL2` whose value is `'webgl2'`,
     * - `WEBGL` whose value is `'webgl'`, or
     * - `P2D` whose value is `'p2d'`. This is the default for 2D sketches.
     * - `P2DHDR` whose value is `'p2d-hdr'` (used for HDR 2D sketches, if available).
     *
     * See <a href="#/p5/setAttributes">setAttributes()</a> for ways to set the
     * WebGL version.
     *
     * @property {(WEBGL|WEBGL2)} webglVersion
     * @readOnly
     * @example
     * function setup() {
     *   background(200);
     *
     *   // Display the current WebGL version.
     *   text(webglVersion, 42, 54);
     *
     *   describe('The text "p2d" written in black on a gray background.');
     * }
     *
     * @example
     * let font;
     *
     * async function setup() {
     *   // Load a font to use.
     *   font = await loadFont('assets/inconsolata.otf');
     *
     *   // Create a canvas using WEBGL mode.
     *   createCanvas(100, 50, WEBGL);
     *   background(200);
     *
     *   // Display the current WebGL version.
     *   fill(0);
     *   textFont(font);
     *   text(webglVersion, -15, 5);
     *
     *   describe('The text "webgl2" written in black on a gray background.');
     * }
     *
     * @example
     * let font;
     *
     * async function setup() {
     *   // Load a font to use.
     *   font = await loadFont('assets/inconsolata.otf');
     *
     *   // Create a canvas using WEBGL mode.
     *   createCanvas(100, 50, WEBGL);
     *
     *   // Set WebGL to version 1.
     *   setAttributes({ version: 1 });
     *
     *   background(200);
     *
     *   // Display the current WebGL version.
     *   fill(0);
     *   textFont(font);
     *   text(webglVersion, -14, 5);
     *
     *   describe('The text "webgl" written in black on a gray background.');
     * }
     */
    fn.webglVersion = P2D;

    /**
     * A `Number` variable that stores the width of the screen display.
     *
     * `displayWidth` is useful for running full-screen programs. Its value
     * depends on the current <a href="#/p5/pixelDensity">pixelDensity()</a>.
     *
     * Note: The actual screen width can be computed as
     * `displayWidth * pixelDensity()`.
     *
     * @property {Number} displayWidth
     * @readOnly
     * @example
     * // META:norender
     * function setup() {
     *   // Set the canvas' width and height
     *   // using the display's dimensions.
     *   createCanvas(displayWidth, displayHeight);
     *
     *   background(200);
     *
     *   describe('A gray canvas that is the same size as the display.');
     * }
     *
     * @alt
     * This example does not render anything.
     */
    fn.displayWidth = screen.width;

    /**
     * A `Number` variable that stores the height of the screen display.
     *
     * `displayHeight` is useful for running full-screen programs. Its value
     * depends on the current <a href="#/p5/pixelDensity">pixelDensity()</a>.
     *
     * Note: The actual screen height can be computed as
     * `displayHeight * pixelDensity()`.
     *
     * @property {Number} displayHeight
     * @readOnly
     * @example
     * // META:norender
     * function setup() {
     *   // Set the canvas' width and height
     *   // using the display's dimensions.
     *   createCanvas(displayWidth, displayHeight);
     *
     *   background(200);
     *
     *   describe('A gray canvas that is the same size as the display.');
     * }
     *
     * @alt
     * This example does not render anything.
     */
    fn.displayHeight = screen.height;

    /**
     * A `Number` variable that stores the width of the browser's viewport.
     *
     * The <a href="https://developer.mozilla.org/en-US/docs/Glossary/Layout_viewport" target="_blank">layout viewport</a>
     * is the area within the browser that's available for drawing.
     *
     * @property {Number} windowWidth
     * @readOnly
     * @example
     * // META:norender
     * function setup() {
     *   // Set the canvas' width and height
     *   // using the browser's dimensions.
     *   createCanvas(windowWidth, windowHeight);
     *
     *   background(200);
     *
     *   describe('A gray canvas that takes up the entire browser window.');
     * }
     *
     * @alt
     * This example does not render anything.
     */
    fn.windowWidth = 0;

    /**
     * A `Number` variable that stores the height of the browser's viewport.
     *
     * The <a href="https://developer.mozilla.org/en-US/docs/Glossary/Layout_viewport" target="_blank">layout viewport</a>
     * is the area within the browser that's available for drawing.
     *
     * @property {Number} windowHeight
     * @readOnly
     * @example
     * // META:norender
     * function setup() {
     *   // Set the canvas' width and height
     *   // using the browser's dimensions.
     *   createCanvas(windowWidth, windowHeight);
     *
     *   background(200);
     *
     *   describe('A gray canvas that takes up the entire browser window.');
     * }
     *
     * @alt
     * This example does not render anything.
     */
    fn.windowHeight = 0;

    /**
     * A function that's called when the browser window is resized.
     *
     * Code placed in the body of `windowResized()` will run when the
     * browser window's size changes. It's a good place to call
     * <a href="#/p5/resizeCanvas">resizeCanvas()</a> or make other
     * adjustments to accommodate the new window size.
     *
     * The `event` parameter is optional. If added to the function declaration, it
     * can be used for debugging or other purposes.
     *
     * @method windowResized
     * @param {Event} [event] optional resize Event.
     * @example
     * // META:norender
     * function setup() {
     *   createCanvas(windowWidth, windowHeight);
     *
     *   describe('A gray canvas with a white circle at its center. The canvas takes up the entire browser window. It changes size to match the browser window.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw a circle at the center.
     *   circle(width / 2, height / 2, 50);
     * }
     *
     * // Resize the canvas when the
     * // browser's size changes.
     * function windowResized() {
     *   resizeCanvas(windowWidth, windowHeight);
     * }
     *
     * @alt
     * This example does not render anything.
     *
     * @example
     * // META:norender
     * function setup() {
     *   createCanvas(windowWidth, windowHeight);
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   describe('A gray canvas that takes up the entire browser window. It changes size to match the browser window.');
     * }
     *
     * function windowResized(event) {
     *   // Resize the canvas when the
     *   // browser's size changes.
     *   resizeCanvas(windowWidth, windowHeight);
     *
     *   // Print the resize event to the console for debugging.
     *   print(event);
     * }
     *
     * @alt
     * This example does not render anything.
     */
    fn._onresize = function(e) {
      this.windowWidth = getWindowWidth();
      this.windowHeight = getWindowHeight();
      let executeDefault;
      if (typeof this._customActions.windowResized === 'function') {
        executeDefault = this._customActions.windowResized(e);
        if (executeDefault !== undefined && !executeDefault) {
          e.preventDefault();
        }
      }
    };

    function getWindowWidth() {
      return (
        window.innerWidth ||
        (document.documentElement && document.documentElement.clientWidth) ||
        (document.body && document.body.clientWidth) ||
        0
      );
    }

    function getWindowHeight() {
      return (
        window.innerHeight ||
        (document.documentElement && document.documentElement.clientHeight) ||
        (document.body && document.body.clientHeight) ||
        0
      );
    }

    /**
     * Called upon each p5 instantiation instead of module import due to the
     * possibility of the window being resized when no sketch is active.
     */
    fn._updateWindowSize = function() {
      this.windowWidth = getWindowWidth();
      this.windowHeight = getWindowHeight();
    };

    Object.defineProperty(fn, 'width', {
      get(){
        return this._renderer.width;
      }
    });

    Object.defineProperty(fn, 'height', {
      get(){
        return this._renderer.height;
      }
    });

    /**
     * Toggles full-screen mode or returns the current mode.
     *
     * Calling `fullscreen(true)` makes the sketch full-screen. Calling
     * `fullscreen(false)` makes the sketch its original size.
     *
     * Calling `fullscreen()` without an argument returns `true` if the sketch
     * is in full-screen mode and `false` if not.
     *
     * Note: Due to browser restrictions, `fullscreen()` can only be called with
     * user input such as a mouse press.
     *
     * @method fullscreen
     * @param  {Boolean} [val] whether the sketch should be in fullscreen mode.
     * @return {Boolean} current fullscreen state.
     * @example
     * function setup() {
     *   background(200);
     *
     *   describe('A gray canvas that switches between default and full-screen display when clicked.');
     * }
     *
     * // If the mouse is pressed,
     * // toggle full-screen mode.
     * function mousePressed() {
     *   if (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) {
     *     let fs = fullscreen();
     *     fullscreen(!fs);
     *   }
     * }
     */
    fn.fullscreen = function(val) {
      // p5._validateParameters('fullscreen', arguments);
      // no arguments, return fullscreen or not
      if (typeof val === 'undefined') {
        return (
          document.fullscreenElement ||
          document.webkitFullscreenElement ||
          document.mozFullScreenElement ||
          document.msFullscreenElement
        );
      } else {
        // otherwise set to fullscreen or not
        if (val) {
          launchFullscreen(document.documentElement);
        } else {
          exitFullscreen();
        }
      }
    };

    /**
     * Sets the pixel density or returns the current density.
     *
     * Computer displays are grids of little lights called <em>pixels</em>. A
     * display's <em>pixel density</em> describes how many pixels it packs into an
     * area. Displays with smaller pixels have a higher pixel density and create
     * sharper images.
     *
     * `pixelDensity()` sets the pixel scaling for high pixel density displays.
     * By default, the pixel density is set to match the display's density.
     * Calling `pixelDensity(1)` turn this off.
     *
     * Calling `pixelDensity()` without an argument returns the current pixel
     * density.
     *
     * @method pixelDensity
     * @param  {Number} [val] desired pixel density.
     * @chainable
     * @example
     * function setup() {
     *   // Set the pixel density to 1.
     *   pixelDensity(1);
     *
     *   // Create a canvas and draw
     *   // a circle.
     *   createCanvas(100, 100);
     *   background(200);
     *   circle(50, 50, 70);
     *
     *   describe('A fuzzy white circle on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   // Set the pixel density to 3.
     *   pixelDensity(3);
     *
     *   // Create a canvas, paint the
     *   // background, and draw a
     *   // circle.
     *   createCanvas(100, 100);
     *   background(200);
     *   circle(50, 50, 70);
     *
     *   describe('A sharp white circle on a gray canvas.');
     * }
     */
    /**
     * @method pixelDensity
     * @returns {Number} current pixel density of the sketch.
     */
    fn.pixelDensity = function(val) {
      // p5._validateParameters('pixelDensity', arguments);
      let returnValue;
      if (typeof val === 'number') {
        if (val !== this._renderer._pixelDensity) {
          this._renderer._pixelDensity = val;
        }
        returnValue = this;
        this.resizeCanvas(this.width, this.height, true); // as a side effect, it will clear the canvas
      } else {
        returnValue = this._renderer._pixelDensity;
      }
      return returnValue;
    };

    /**
     * Returns the display's current pixel density.
     *
     * @method displayDensity
     * @returns {Number} current pixel density of the display.
     * @example
     * function setup() {
     *   // Set the pixel density to 1.
     *   pixelDensity(1);
     *
     *   // Create a canvas and draw
     *   // a circle.
     *   createCanvas(100, 100);
     *   background(200);
     *   circle(50, 50, 70);
     *
     *   describe('A fuzzy white circle drawn on a gray background. The circle becomes sharper when the mouse is pressed.');
     * }
     *
     * function mousePressed() {
     *   // Get the current display density.
     *   let d = displayDensity();
     *
     *   // Use the display density to set
     *   // the sketch's pixel density.
     *   pixelDensity(d);
     *
     *   // Paint the background and
     *   // draw a circle.
     *   background(200);
     *   circle(50, 50, 70);
     * }
     */
    fn.displayDensity = () => window.devicePixelRatio;

    function launchFullscreen(element) {
      const enabled =
        document.fullscreenEnabled ||
        document.webkitFullscreenEnabled ||
        document.mozFullScreenEnabled ||
        document.msFullscreenEnabled;
      if (!enabled) {
        throw new Error('Fullscreen not enabled in this browser.');
      }
      if (element.requestFullscreen) {
        element.requestFullscreen();
      } else if (element.mozRequestFullScreen) {
        element.mozRequestFullScreen();
      } else if (element.webkitRequestFullscreen) {
        element.webkitRequestFullscreen();
      } else if (element.msRequestFullscreen) {
        element.msRequestFullscreen();
      }
    }

    function exitFullscreen() {
      if (document.exitFullscreen) {
        document.exitFullscreen();
      } else if (document.mozCancelFullScreen) {
        document.mozCancelFullScreen();
      } else if (document.webkitExitFullscreen) {
        document.webkitExitFullscreen();
      } else if (document.msExitFullscreen) {
        document.msExitFullscreen();
      }
    }

    /**
     * Returns the sketch's current
     * <a href="https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Web_mechanics/What_is_a_URL" target="_blank">URL</a>
     * as a `String`.
     *
     * @method getURL
     * @return {String} url
     * @example
     * function setup() {
     *   background(200);
     *
     *   // Get the sketch's URL
     *   // and display it.
     *   let url = getURL();
     *   textWrap(CHAR);
     *   text(url, 0, 40, 100);
     *
     *   describe('The URL "https://p5js.org/reference/p5/getURL" written in black on a gray background.');
     * }
     */
    fn.getURL = () => location.href;

    /**
     * Returns the current
     * <a href="https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Web_mechanics/What_is_a_URL#path_to_resource" target="_blank">URL</a>
     * path as an `Array` of `String`s.
     *
     * For example, consider a sketch hosted at the URL
     * `https://example.com/sketchbook`. Calling `getURLPath()` returns
     * `['sketchbook']`. For a sketch hosted at the URL
     * `https://example.com/sketchbook/monday`, `getURLPath()` returns
     * `['sketchbook', 'monday']`.
     *
     * @method getURLPath
     * @return {String[]} path components.
     * @example
     * function setup() {
     *   background(200);
     *
     *   // Get the sketch's URL path
     *   // and display the first
     *   // part.
     *   let path = getURLPath();
     *   text(path[0], 25, 54);
     *
     *   describe('The word "reference" written in black on a gray background.');
     * }
     */
    fn.getURLPath = () =>
      location.pathname.split('/').filter(v => v !== '');

    /**
     * Returns the current
     * <a href="https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Web_mechanics/What_is_a_URL#parameters" target="_blank">URL parameters</a>
     * in an `Object`.
     *
     * For example, calling `getURLParams()` in a sketch hosted at the URL
     * `https://p5js.org?year=2014&month=May&day=15` returns
     * `{ year: 2014, month: 'May', day: 15 }`.
     *
     * @method getURLParams
     * @return {Object} URL params
     * @example
     * // META:norender
     * // Imagine this sketch is hosted at the following URL:
     * // https://p5js.org?year=2014&month=May&day=15
     *
     * function setup() {
     *   background(200);
     *
     *   // Get the sketch's URL
     *   // parameters and display
     *   // them.
     *   let params = getURLParams();
     *   text(params.day, 10, 20);
     *   text(params.month, 10, 40);
     *   text(params.year, 10, 60);
     *
     *   describe('The text "15", "May", and "2014" written in black on separate lines.');
     * }
     *
     * @alt
     * This example does not render anything.
     */
    fn.getURLParams = function() {
      const re = /[?&]([^&=]+)(?:[&=])([^&=]+)/gim;
      let m;
      const v = {};
      while ((m = re.exec(location.search)) != null) {
        if (m.index === re.lastIndex) {
          re.lastIndex++;
        }
        v[m[1]] = m[2];
      }
      return v;
    };

    /**
     * Converts 3D world coordinates to 2D screen coordinates.
     *
     * This function takes a 3D vector and converts its coordinates
     * from the world space to screen space. This can be useful for placing
     * 2D elements in a 3D scene or for determining the screen position
     * of 3D objects.
     *
     * @method worldToScreen
     * @param {Number|p5.Vector} x The x coordinate in world space. (Or a vector for all three coordinates.)
     * @param {Number} y The y coordinate in world space.
     * @param {Number} [z] The z coordinate in world space.
     * @return {p5.Vector} A vector containing the 2D screen coordinates.
     * @example
     * function setup() {
     *   createCanvas(150, 150);
     *   let vertices = [
     *     createVector(-20, -20),
     *     createVector(20, -20),
     *     createVector(20, 20),
     *     createVector(-20, 20)
     *   ];
     *
     *   push();
     *   translate(75, 55);
     *   rotate(PI / 4);
     *
     *   // Convert world coordinates to screen coordinates
     *   let screenPos = vertices.map(v => worldToScreen(v));
     *   pop();
     *
     *   background(200);
     *
     *   stroke(0);
     *   fill(100, 150, 255, 100);
     *   beginShape();
     *   screenPos.forEach(pos => vertex(pos.x, pos.y));
     *   endShape(CLOSE);
     *
     *   screenPos.forEach((pos, i) => {
     *     fill(0);
     *     textSize(10);
     *     if (i === 0) {
     *       text(i + 1, pos.x + 3, pos.y - 7);
     *     } else if (i === 1) {
     *       text(i + 1, pos.x + 7, pos.y + 2);
     *     } else if (i === 2) {
     *       text(i + 1, pos.x - 2, pos.y + 12);
     *     } else if (i === 3) {
     *       text(i + 1, pos.x - 12, pos.y - 2);
     *     }
     *   });
     *
     *   fill(0);
     *   noStroke();
     *   textSize(10);
     *   let legendY = height - 35;
     *   screenPos.forEach((pos, i) => {
     *     text(`Vertex ${i + 1}: (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)})`, 5, legendY + i * 10);
     *   });
     *
     *   describe('A rotating square is transformed and drawn using screen coordinates.');
     *
     * }
     *
     * @example
     * let vertices;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *   vertices = [
     *     createVector(-25, -25, -25),
     *     createVector(25, -25, -25),
     *     createVector(25, 25, -25),
     *     createVector(-25, 25, -25),
     *     createVector(-25, -25, 25),
     *     createVector(25, -25, 25),
     *     createVector(25, 25, 25),
     *     createVector(-25, 25, 25)
     *   ];
     *
     *   describe('A rotating cube with points mapped to 2D screen space and displayed as ellipses.');
     *
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Animate rotation
     *   let rotationX = millis() / 1000;
     *   let rotationY = millis() / 1200;
     *
     *   push();
     *
     *   rotateX(rotationX);
     *   rotateY(rotationY);
     *
     *   // Convert world coordinates to screen coordinates
     *   let screenPos = vertices.map(v => worldToScreen(v));
     *
     *   pop();
     *
     *   screenPos.forEach((pos, i) => {
     *
     *     let screenX = pos.x - width / 2;
     *     let screenY = pos.y - height / 2;
     *     fill(0);
     *     noStroke();
     *     ellipse(screenX, screenY, 3, 3);
     *   });
     * }
     */
    fn.worldToScreen = function(worldPosition) {
      if (typeof worldPosition === 'number') {
        // We got passed numbers, convert to vector
        worldPosition = this.createVector(...arguments);
      }

      const matrix = this._renderer.getWorldToScreenMatrix();
      const screenPosition = matrix.multiplyAndNormalizePoint(worldPosition);
      return screenPosition;
    };

    /**
     * Converts 2D screen coordinates to 3D world coordinates.
     *
     * This function takes a vector and converts its coordinates from coordinates
     * on the screen to coordinates in the currently drawn object. This can be
     * useful for determining the mouse position relative to a 2D or 3D object.
     *
     * If given, the Z component of the input coordinates is treated as "depth",
     * or distance from the camera.
     *
     * @method screenToWorld
     * @param {Number|p5.Vector} x The x coordinate in screen space. (Or a vector for all three coordinates.)
     * @param {Number} y The y coordinate in screen space.
     * @param {Number} [z] The z coordinate in screen space.
     * @return {p5.Vector} A vector containing the 3D world space coordinates.
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *   describe('A rotating square with a line passing through the mouse drawn across it.');
     * }
     *
     * function draw() {
     *   background(220);
     *
     *   // Move to center and rotate
     *   translate(width/2, height/2);
     *   rotate(millis() / 1000);
     *   rect(-30, -30, 60);
     *
     *   // Compute the location of the mouse in the coordinates of the square
     *   let localMouse = screenToWorld(createVector(mouseX, mouseY));
     *
     *   // Draw a line parallel to the local Y axis, passing through the mouse
     *   line(localMouse.x, -30, localMouse.x, 30);
     * }
     */
    fn.screenToWorld = function(screenPosition) {
      if (typeof screenPosition === 'number') {
        // We got passed numbers, convert to vector
        screenPosition = this.createVector(...arguments);
      }

      const matrix = this._renderer.getWorldToScreenMatrix();

      if (screenPosition.dimensions === 2) {
        // Calculate a sensible Z value for the current camera projection that
        // will result in 0 once converted to world coordinates
        let z = matrix.mat4[14] / matrix.mat4[15];
        screenPosition = this.createVector(screenPosition.x, screenPosition.y, z);
      }

      const matrixInverse = matrix.invert(matrix);

      const worldPosition = matrixInverse
        .multiplyAndNormalizePoint(screenPosition);
      return worldPosition;
    };

    /**
     * A `Number` variable that stores the width of the canvas in pixels.
     *
     * `width`'s default value is 100. Calling
     * <a href="#/p5/createCanvas">createCanvas()</a> or
     * <a href="#/p5/resizeCanvas">resizeCanvas()</a> changes the value of
     * `width`. Calling <a href="#/p5/noCanvas">noCanvas()</a> sets its value to
     * 0.
     *
     * @example
     * function setup() {
     *   background(200);
     *
     *   // Display the canvas' width.
     *   text(width, 42, 54);
     *
     *   describe('The number 100 written in black on a gray square.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(50, 100);
     *
     *   background(200);
     *
     *   // Display the canvas' width.
     *   text(width, 21, 54);
     *
     *   describe('The number 50 written in black on a gray rectangle.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Display the canvas' width.
     *   text(width, 42, 54);
     *
     *   describe('The number 100 written in black on a gray square. When the mouse is pressed, the square becomes a rectangle and the number becomes 50.');
     * }
     *
     * // If the mouse is pressed, reisze
     * // the canvas and display its new
     * // width.
     * function mousePressed() {
     *   if (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) {
     *     resizeCanvas(50, 100);
     *     background(200);
     *     text(width, 21, 54);
     *   }
     * }
     *
     * @property {Number} width
     * @readOnly
     */

    /**
     * A `Number` variable that stores the height of the canvas in pixels.
     *
     * `height`'s default value is 100. Calling
     * <a href="#/p5/createCanvas">createCanvas()</a> or
     * <a href="#/p5/resizeCanvas">resizeCanvas()</a> changes the value of
     * `height`. Calling <a href="#/p5/noCanvas">noCanvas()</a> sets its value to
     * 0.
     *
     * @example
     * function setup() {
     *   background(200);
     *
     *   // Display the canvas' height.
     *   text(height, 42, 54);
     *
     *   describe('The number 100 written in black on a gray square.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 50);
     *
     *   background(200);
     *
     *   // Display the canvas' height.
     *   text(height, 42, 27);
     *
     *   describe('The number 50 written in black on a gray rectangle.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Display the canvas' height.
     *   text(height, 42, 54);
     *
     *   describe('The number 100 written in black on a gray square. When the mouse is pressed, the square becomes a rectangle and the number becomes 50.');
     * }
     *
     * // If the mouse is pressed, reisze
     * // the canvas and display its new
     * // height.
     * function mousePressed() {
     *   if (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) {
     *     resizeCanvas(100, 50);
     *     background(200);
     *     text(height, 42, 27);
     *   }
     * }
     *
     * @property {Number} height
     * @readOnly
     */
  }

  if(typeof p5 !== 'undefined'){
    environment$1(p5, p5.prototype);
  }

  /** @import { Matrix3x3, Vector3 } from "./types.js" */


  // dot3 and transform functions adapted from https://github.com/texel-org/color/blob/9793c7d4d02b51f068e0f3fd37131129a4270396/src/core.js
  //
  // The MIT License (MIT)
  // Copyright (c) 2024 Matt DesLauriers

  // 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.

  /**
   * Returns the dot product of two vectors each with a length of 3.
   *
   * @param {Vector3} a
   * @param {Vector3} b
   * @returns {number}
   */
  function dot3 (a, b) {
  	return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
  }

  /**
   * Transforms a vector of length 3 by a 3x3 matrix. Specify the same input and output
   * vector to transform in place.
   *
   * @param {Vector3} input
   * @param {Matrix3x3} matrix
   * @param {Vector3} [out]
   * @returns {Vector3}
   */
  function multiply_v3_m3x3 (input, matrix, out = [0, 0, 0]) {
  	const x = dot3(input, matrix[0]);
  	const y = dot3(input, matrix[1]);
  	const z = dot3(input, matrix[2]);
  	out[0] = x;
  	out[1] = y;
  	out[2] = z;
  	return out;
  }

  /**
   * Various utility functions
   */


  /**
   * Check if a value is a string (including a String object)
   * @param {any} str - Value to check
   * @returns {str is string}
   */
  function isString (str) {
  	return type$1(str) === "string";
  }

  /**
   * Determine the internal JavaScript [[Class]] of an object.
   * @param {any} o - Value to check
   * @returns {string}
   */
  function type$1 (o) {
  	let str = Object.prototype.toString.call(o);

  	return (str.match(/^\[object\s+(.*?)\]$/)[1] || "").toLowerCase();
  }

  /**
   * @param {number} n
   * @param {{ precision?: number | undefined, unit?: string | undefined }} options
   * @returns {string}
   */
  function serializeNumber (n, { precision = 16, unit }) {
  	if (isNone(n)) {
  		return "none";
  	}

  	n = +toPrecision(n, precision);

  	return n + (unit ?? "");
  }

  /**
   * Check if a value corresponds to a none argument
   * @param {any} n - Value to check
   * @returns {n is null}
   */
  function isNone (n) {
  	return n === null;
  }

  /**
   * Round a number to a certain number of significant digits
   * @param {number} n - The number to round
   * @param {number} precision - Number of significant digits
   */
  function toPrecision (n, precision) {
  	if (n === 0) {
  		return 0;
  	}
  	let integer = ~~n;
  	let digits = 0;
  	if (integer && precision) {
  		digits = ~~Math.log10(Math.abs(integer)) + 1;
  	}
  	const multiplier = 10.0 ** (precision - digits);
  	return Math.floor(n * multiplier + 0.5) / multiplier;
  }

  /**
   * @param {number} start
   * @param {number} end
   * @param {number} p
   */
  function interpolate (start, end, p) {
  	if (isNaN(start)) {
  		return end;
  	}

  	if (isNaN(end)) {
  		return start;
  	}

  	return start + (end - start) * p;
  }

  /**
   * @param {number} start
   * @param {number} end
   * @param {number} value
   */
  function interpolateInv (start, end, value) {
  	return (value - start) / (end - start);
  }

  /**
   * @param {[number, number]} from
   * @param {[number, number]} to
   * @param {number} value
   */
  function mapRange (from, to, value) {
  	if (
  		!from ||
  		!to ||
  		from === to ||
  		(from[0] === to[0] && from[1] === to[1]) ||
  		isNaN(value) ||
  		value === null
  	) {
  		// Ranges missing or the same
  		return value;
  	}

  	return interpolate(to[0], to[1], interpolateInv(from[0], from[1], value));
  }

  /**
   * Clamp value between the minimum and maximum
   * @param {number} min minimum value to return
   * @param {number} val the value to return if it is between min and max
   * @param {number} max maximum value to return
   */
  function clamp (min, val, max) {
  	return Math.max(Math.min(max, val), min);
  }

  /**
   * Copy sign of one value to another.
   * @param {number} to - Number to copy sign to
   * @param {number} from - Number to copy sign from
   */
  function copySign (to, from) {
  	return Math.sign(to) === Math.sign(from) ? to : -to;
  }

  /**
   * Perform pow on a signed number and copy sign to result
   * @param {number} base The base number
   * @param {number} exp The exponent
   */
  function spow (base, exp) {
  	return copySign(Math.abs(base) ** exp, base);
  }

  /**
   * Perform a divide, but return zero if the denominator is zero
   * @param {number} n The numerator
   * @param {number} d The denominator
   */
  function zdiv (n, d) {
  	return d === 0 ? 0 : n / d;
  }

  /**
   * Perform a bisect on a sorted list and locate the insertion point for
   * a value in arr to maintain sorted order.
   * @param {number[]} arr - array of sorted numbers
   * @param {number} value - value to find insertion point for
   * @param {number} lo - used to specify a the low end of a subset of the list
   * @param {number} hi - used to specify a the high end of a subset of the list
   */
  function bisectLeft (arr, value, lo = 0, hi = arr.length) {
  	while (lo < hi) {
  		const mid = (lo + hi) >> 1;
  		if (arr[mid] < value) {
  			lo = mid + 1;
  		}
  		else {
  			hi = mid;
  		}
  	}
  	return lo;
  }

  /**
   * Determines whether an argument is an instance of a constructor, including subclasses.
   * This is done by first just checking `instanceof`,
   * and then comparing the string names of the constructors if that fails.
   * @param {any} arg
   * @param {C} constructor
   * @template {new (...args: any) => any} C
   * @returns {arg is InstanceType<C>}
   */
  function isInstance (arg, constructor) {
  	if (arg instanceof constructor) {
  		return true;
  	}

  	const targetName = constructor.name;

  	while (arg) {
  		const proto = Object.getPrototypeOf(arg);
  		const constructorName = proto?.constructor?.name;
  		if (constructorName === targetName) {
  			return true;
  		}
  		if (!constructorName || constructorName === "Object") {
  			return false;
  		}
  		arg = proto;
  	}

  	return false;
  }

  class Type {
  	// Class properties - declared here so that type inference works
  	type;
  	coordMeta;
  	coordRange;
  	/** @type {[number, number]} */
  	range;

  	/**
  	 * @param {any} type
  	 * @param {import("./types.js").CoordMeta} coordMeta
  	 */
  	constructor (type, coordMeta) {
  		if (typeof type === "object") {
  			this.coordMeta = type;
  		}

  		if (coordMeta) {
  			this.coordMeta = coordMeta;
  			this.coordRange = coordMeta.range ?? coordMeta.refRange;
  		}

  		if (typeof type === "string") {
  			let params = type
  				.trim()
  				.match(/^(?<type><[a-z]+>)(\[(?<min>-?[.\d]+),\s*(?<max>-?[.\d]+)\])?$/);

  			if (!params) {
  				throw new TypeError(`Cannot parse ${type} as a type definition.`);
  			}

  			this.type = params.groups.type;
  			let { min, max } = params.groups;

  			if (min || max) {
  				this.range = [+min, +max];
  			}
  		}
  	}

  	/** @returns {[number, number]} */
  	get computedRange () {
  		if (this.range) {
  			return this.range;
  		}
  		if (this.type === "<percentage>") {
  			return this.percentageRange();
  		}
  		else if (this.type === "<angle>") {
  			return [0, 360];
  		}
  		return null;
  	}

  	get unit () {
  		if (this.type === "<percentage>") {
  			return "%";
  		}
  		else if (this.type === "<angle>") {
  			return "deg";
  		}

  		return "";
  	}

  	/**
  	 * Map a number to the internal representation
  	 * @param {number} number
  	 */
  	resolve (number) {
  		if (this.type === "<angle>") {
  			return number;
  		}

  		let fromRange = this.computedRange;
  		let toRange = this.coordRange;

  		if (this.type === "<percentage>") {
  			toRange ??= this.percentageRange();
  		}

  		return mapRange(fromRange, toRange, number);
  	}

  	/**
  	 * Serialize a number from the internal representation to a string
  	 * @param {number} number
  	 * @param {number} [precision]
  	 */
  	serialize (number, precision) {
  		let toRange = this.type === "<percentage>" ? this.percentageRange(100) : this.computedRange;

  		let unit = this.unit;

  		number = mapRange(this.coordRange, toRange, number);
  		return serializeNumber(number, { unit, precision });
  	}

  	toString () {
  		let ret = this.type;

  		if (this.range) {
  			let [min = "", max = ""] = this.range;
  			ret += `[${min},${max}]`;
  		}

  		return ret;
  	}

  	/**
  	 * Returns a percentage range for values of this type
  	 * @param {number} scale
  	 * @returns {[number, number]}
  	 */
  	percentageRange (scale = 1) {
  		let range;
  		if (
  			(this.coordMeta && this.coordMeta.range) ||
  			(this.coordRange && this.coordRange[0] >= 0)
  		) {
  			range = [0, 1];
  		}
  		else {
  			range = [-1, 1];
  		}
  		return [range[0] * scale, range[1] * scale];
  	}

  	static get (type, coordMeta) {
  		if (isInstance(type, this)) {
  			return type;
  		}

  		return new this(type, coordMeta);
  	}
  }

  /** @import { ColorSpace, Coords } from "./types.js" */

  // Type re-exports
  /** @typedef {import("./types.js").Format} FormatInterface */

  /**
   * @internal
   * Used to index {@link FormatInterface Format} objects and store an instance.
   * Not meant for external use
   */
  const instance = Symbol("instance");

  /**
   * Remove the first element of an array type
   * @template {any[]} T
   * @typedef {T extends [any, ...infer R] ? R : T[number][]} RemoveFirstElement
   */

  /**
   * @class Format
   * @implements {Omit<FormatInterface, "coords" | "serializeCoords">}
   * Class to hold a color serialization format
   */
  class Format {
  	// Class properties - declared here so that type inference works
  	type;
  	name;
  	spaceCoords;
  	/** @type {Type[][]} */
  	coords;
  	/** @type {string | undefined} */
  	id;
  	/** @type {boolean | undefined} */
  	alpha;

  	/**
  	 * @param {FormatInterface} format
  	 * @param {ColorSpace} space
  	 */
  	constructor (format, space = format.space) {
  		format[instance] = this;
  		this.type = "function";
  		this.name = "color";

  		Object.assign(this, format);

  		this.space = space;

  		if (this.type === "custom") {
  			// Nothing else to do here
  			return;
  		}

  		this.spaceCoords = Object.values(space.coords);

  		if (!this.coords) {
  			// @ts-expect-error Strings are converted to the correct type later
  			this.coords = this.spaceCoords.map(coordMeta => {
  				let ret = ["<number>", "<percentage>"];

  				if (coordMeta.type === "angle") {
  					ret.push("<angle>");
  				}

  				return ret;
  			});
  		}

  		this.coords = this.coords.map(
  			/** @param {string | string[] | Type[]} types */ (types, i) => {
  				let coordMeta = this.spaceCoords[i];

  				if (typeof types === "string") {
  					types = types.trim().split(/\s*\|\s*/);
  				}

  				return types.map(type => Type.get(type, coordMeta));
  			},
  		);
  	}

  	/**
  	 * @param {Coords} coords
  	 * @param {number} precision
  	 * @param {Type[]} types
  	 */
  	serializeCoords (coords, precision, types) {
  		types = coords.map((_, i) =>
  			Type.get(types?.[i] ?? this.coords[i][0], this.spaceCoords[i]));
  		return coords.map((c, i) => types[i].serialize(c, precision));
  	}

  	/**
  	 * Validates the coordinates of a color against a format's coord grammar and
  	 * maps the coordinates to the range or refRange of the coordinates.
  	 * @param {Coords} coords
  	 * @param {[string, string, string]} types
  	 */
  	coerceCoords (coords, types) {
  		return Object.entries(this.space.coords).map(([id, coordMeta], i) => {
  			let arg = coords[i];

  			if (isNone(arg) || isNaN(arg)) {
  				// Nothing to do here
  				return arg;
  			}

  			// Find grammar alternative that matches the provided type
  			// Non-strict equals is intentional because we are comparing w/ string objects
  			let providedType = types[i];
  			let type = this.coords[i].find(c => c.type == providedType);

  			// Check that each coord conforms to its grammar
  			if (!type) {
  				// Type does not exist in the grammar, throw
  				let coordName = coordMeta.name || id;
  				throw new TypeError(
  					`${providedType ?? /** @type {any} */ (arg)?.raw ?? arg} not allowed for ${coordName} in ${this.name}()`,
  				);
  			}

  			arg = type.resolve(arg);

  			if (type.range) {
  				// Adjust type to include range
  				types[i] = type.toString();
  			}

  			return arg;
  		});
  	}

  	/**
  	 * @returns {boolean | Required<FormatInterface>["serialize"]}
  	 */
  	canSerialize () {
  		return this.type === "function" || /** @type {any} */ (this).serialize;
  	}

  	/**
  	 * @param {string} str
  	 * @returns {(import("./types.js").ColorConstructor) | undefined | null}
  	 */
  	parse (str) {
  		return null;
  	}

  	/**
  	 * @param {Format | FormatInterface} format
  	 * @param {RemoveFirstElement<ConstructorParameters<typeof Format>>} args
  	 * @returns {Format}
  	 */
  	static get (format, ...args) {
  		if (!format || isInstance(format, this)) {
  			return /** @type {Format} */ (format);
  		}

  		if (format[instance]) {
  			return format[instance];
  		}

  		return new Format(format, ...args);
  	}
  }

  /**
   * A class for adding deep extensibility to any piece of JS code
   */
  class Hooks {
  	add (name, callback, first) {
  		if (typeof arguments[0] != "string") {
  			// Multiple hooks
  			for (var name in arguments[0]) {
  				this.add(name, arguments[0][name], arguments[1]);
  			}

  			return;
  		}

  		(Array.isArray(name) ? name : [name]).forEach(function (name) {
  			this[name] = this[name] || [];

  			if (callback) {
  				this[name][first ? "unshift" : "push"](callback);
  			}
  		}, this);
  	}

  	run (name, env) {
  		this[name] = this[name] || [];
  		this[name].forEach(function (callback) {
  			callback.call(env && env.context ? env.context : env, env);
  		});
  	}
  }

  /**
   * The instance of {@link Hooks} used throughout Color.js
   */
  const hooks = new Hooks();

  // Type re-exports
  /** @typedef {import("./types.js").White} White */

  /** @type {Record<string, White>} */
  // prettier-ignore
  const WHITES = {
  	// for compatibility, the four-digit chromaticity-derived ones everyone else uses
  	D50: [0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585],
  	D65: [0.3127 / 0.3290, 1.00000, (1.0 - 0.3127 - 0.3290) / 0.3290],
  };

  /**
   *
   * @param {string | White} name
   * @returns {White}
   */
  function getWhite (name) {
  	if (Array.isArray(name)) {
  		return name;
  	}

  	return WHITES[name];
  }

  /**
   * Adapt XYZ from white point W1 to W2
   * @param {White | string} W1
   * @param {White | string} W2
   * @param {[number, number, number]} XYZ
   * @param {{ method?: string | undefined }} options
   * @returns {[number, number, number]}
   */
  function adapt$1 (W1, W2, XYZ, options = {}) {
  	W1 = getWhite(W1);
  	W2 = getWhite(W2);

  	if (!W1 || !W2) {
  		throw new TypeError(
  			`Missing white point to convert ${!W1 ? "from" : ""}${!W1 && !W2 ? "/" : ""}${!W2 ? "to" : ""}`,
  		);
  	}

  	if (W1 === W2) {
  		// Same whitepoints, no conversion needed
  		return XYZ;
  	}

  	let env = { W1, W2, XYZ, options };

  	hooks.run("chromatic-adaptation-start", env);

  	if (!env.M) {
  		if (env.W1 === WHITES.D65 && env.W2 === WHITES.D50) {
  			// prettier-ignore
  			env.M = [
  				[  1.0479297925449969,   0.022946870601609652, -0.05019226628920524  ],
  				[  0.02962780877005599,  0.9904344267538799,   -0.017073799063418826 ],
  				[ -0.009243040646204504, 0.015055191490298152,  0.7518742814281371   ],
  			];
  		}
  		else if (env.W1 === WHITES.D50 && env.W2 === WHITES.D65) {
  			// prettier-ignore
  			env.M = [
  				[  0.955473421488075,    -0.02309845494876471,  0.06325924320057072  ],
  				[ -0.0283697093338637,    1.0099953980813041,   0.021041441191917323 ],
  				[  0.012314014864481998, -0.020507649298898964, 1.330365926242124    ],
  			];
  		}
  	}

  	hooks.run("chromatic-adaptation-end", env);

  	if (env.M) {
  		return multiply_v3_m3x3(env.XYZ, env.M);
  	}
  	else {
  		throw new TypeError("Only Bradford CAT with white points D50 and D65 supported for now.");
  	}
  }

  // Global defaults one may want to configure
  var defaults$1 = {
  	gamut_mapping: "css",
  	precision: 5,
  	deltaE: "76", // Default deltaE method
  	verbose: globalThis?.process?.env?.NODE_ENV?.toLowerCase() !== "test",
  	warn: function warn (msg) {
  		if (this.verbose) {
  			globalThis?.console?.warn?.(msg);
  		}
  	},
  };

  /** @import { ColorConstructor } from "./types.js" */

  // Type re-exports
  /** @typedef {import("./types.js").ArgumentMeta} ArgumentMeta */
  /** @typedef {import("./types.js").ParseFunctionReturn} ParseFunctionReturn */
  /** @typedef {import("./types.js").ParseOptions} ParseOptions */

  /**
   * Convert a CSS Color string to a color object
   * @param {string} str
   * @param {ParseOptions} [options]
   * @returns {ColorConstructor}
   */
  function parse$4 (str, options) {
  	let env = {
  		str: String(str)?.trim(),
  		options,
  	};

  	hooks.run("parse-start", env);

  	if (env.color) {
  		return env.color;
  	}

  	env.parsed = parseFunction(env.str);
  	let ret;
  	let meta = env.options ? (env.options.parseMeta ?? env.options.meta) : null;

  	if (env.parsed) {
  		// Is a functional syntax
  		let name = env.parsed.name;
  		let format;
  		let space;
  		let coords = env.parsed.args;
  		let types = coords.map((c, i) => env.parsed.argMeta[i]?.type);

  		if (name === "color") {
  			// color() function
  			let id = coords.shift();
  			types.shift();
  			// Check against both <dashed-ident> and <ident> versions
  			let alternateId = id.startsWith("--") ? id.substring(2) : `--${id}`;
  			let ids = [id, alternateId];
  			format = ColorSpace.findFormat({ name, id: ids, type: "function" });

  			if (!format) {
  				// Not found
  				let didYouMean;

  				let registryId = id in ColorSpace.registry ? id : alternateId;
  				if (registryId in ColorSpace.registry) {
  					// Used color space id instead of color() id, these are often different
  					let cssId = ColorSpace.registry[registryId].formats?.color?.id;

  					if (cssId) {
  						let altColor = str.replace("color(" + id, "color(" + cssId);
  						didYouMean = `Did you mean ${altColor}?`;
  					}
  				}

  				throw new TypeError(
  					`Cannot parse ${env.str}. ` + (didYouMean ?? "Missing a plugin?"),
  				);
  			}

  			space = format.space;

  			if (format.id.startsWith("--") && !id.startsWith("--")) {
  				defaults$1.warn(
  					`${space.name} is a non-standard space and not currently supported in the CSS spec. ` +
  						`Use prefixed color(${format.id}) instead of color(${id}).`,
  				);
  			}
  			if (id.startsWith("--") && !format.id.startsWith("--")) {
  				defaults$1.warn(
  					`${space.name} is a standard space and supported in the CSS spec. ` +
  						`Use color(${format.id}) instead of prefixed color(${id}).`,
  				);
  			}
  		}
  		else {
  			format = ColorSpace.findFormat({ name, type: "function" });
  			space = format.space;
  		}

  		if (meta) {
  			Object.assign(meta, {
  				format,
  				formatId: format.name,
  				types,
  				commas: env.parsed.commas,
  			});
  		}

  		let alpha = 1;

  		if (env.parsed.lastAlpha) {
  			alpha = env.parsed.args.pop();

  			if (meta) {
  				meta.alphaType = types.pop();
  			}
  		}

  		let coordCount = format.coords.length;

  		if (coords.length !== coordCount) {
  			throw new TypeError(
  				`Expected ${coordCount} coordinates for ${space.id} in ${env.str}), got ${coords.length}`,
  			);
  		}

  		coords = format.coerceCoords(coords, types);

  		ret = { spaceId: space.id, coords, alpha };
  	}
  	else {
  		// Custom, colorspace-specific format
  		spaceloop: for (let space of ColorSpace.all) {
  			for (let formatId in space.formats) {
  				let format = space.formats[formatId];

  				if (format.type !== "custom") {
  					continue;
  				}

  				if (format.test && !format.test(env.str)) {
  					continue;
  				}

  				// Convert to Format object
  				let formatObject = space.getFormat(format);

  				let color = formatObject.parse(env.str);

  				if (color) {
  					if (meta) {
  						Object.assign(meta, { format: formatObject, formatId });
  					}

  					ret = color;
  					break spaceloop;
  				}
  			}
  		}
  	}

  	if (!ret) {
  		// If we're here, we couldn't parse
  		throw new TypeError(`Could not parse ${str} as a color. Missing a plugin?`);
  	}

  	// Clamp alpha to [0, 1]
  	ret.alpha = isNone(ret.alpha)
  		? ret.alpha
  		: ret.alpha === undefined
  			? 1
  			: clamp(0, ret.alpha, 1);

  	return ret;
  }

  /**
   * Units and multiplication factors for the internally stored numbers
   */
  const units = {
  	"%": 0.01,
  	deg: 1,
  	grad: 0.9,
  	rad: 180 / Math.PI,
  	turn: 360,
  };

  const regex = {
  	// Need to list calc(NaN) explicitly as otherwise its ending paren would terminate the function call
  	function: /^([a-z]+)\(((?:calc\(NaN\)|.)+?)\)$/i,
  	number: /^([-+]?(?:[0-9]*\.)?[0-9]+(e[-+]?[0-9]+)?)$/i,
  	unitValue: RegExp(`(${Object.keys(units).join("|")})$`),

  	// NOTE The -+ are not just for prefix, but also for idents, and e+N notation!
  	singleArgument: /\/?\s*(none|NaN|calc\(NaN\)|[-+\w.]+(?:%|deg|g?rad|turn)?)/g,
  };

  /**
   * Parse a single function argument
   * @param {string} rawArg
   * @returns {{value: number, meta: ArgumentMeta}}
   */
  function parseArgument (rawArg) {
  	/** @type {Partial<ArgumentMeta>} */
  	let meta = {};
  	let unit = rawArg.match(regex.unitValue)?.[0];
  	/** @type {string | number} */
  	let value = (meta.raw = rawArg);

  	if (unit) {
  		// It’s a dimension token
  		meta.type = unit === "%" ? "<percentage>" : "<angle>";
  		meta.unit = unit;
  		meta.unitless = Number(value.slice(0, -unit.length)); // unitless number

  		value = meta.unitless * units[unit];
  	}
  	else if (regex.number.test(value)) {
  		// It's a number
  		// Convert numerical args to numbers
  		value = Number(value);
  		meta.type = "<number>";
  	}
  	else if (value === "none") {
  		value = null;
  	}
  	else if (value === "NaN" || value === "calc(NaN)") {
  		value = NaN;
  		meta.type = "<number>";
  	}
  	else {
  		meta.type = "<ident>";
  	}

  	return { value: /** @type {number} */ (value), meta: /** @type {ArgumentMeta} */ (meta) };
  }

  /**
   * Parse a CSS function, regardless of its name and arguments
   * @param {string} str String to parse
   * @return {ParseFunctionReturn | void}
   */
  function parseFunction (str) {
  	if (!str) {
  		return;
  	}

  	str = str.trim();

  	let parts = str.match(regex.function);

  	if (parts) {
  		// It is a function, parse args
  		let args = [];
  		let argMeta = [];
  		let lastAlpha = false;
  		let name = parts[1].toLowerCase();

  		let separators = parts[2].replace(regex.singleArgument, ($0, rawArg) => {
  			let { value, meta } = parseArgument(rawArg);

  			if (
  				// If there's a slash here, it's modern syntax
  				$0.startsWith("/") ||
  				// If there's still elements to process after there's already 3 in `args` (and the we're not dealing with "color()"), it's likely to be a legacy color like "hsl(0, 0%, 0%, 0.5)"
  				(name !== "color" && args.length === 3)
  			) {
  				// It's alpha
  				lastAlpha = true;
  			}

  			args.push(value);
  			argMeta.push(meta);
  			return "";
  		});

  		return {
  			name,
  			args,
  			argMeta,
  			lastAlpha,
  			commas: separators.includes(","),
  			rawName: parts[1],
  			rawArgs: parts[2],
  		};
  	}
  }

  /** @import { ColorTypes, ParseOptions as GetColorOptions, PlainColorObject } from "./types.js" */

  /**
   * Resolves a color reference (object or string) to a plain color object
   * @overload
   * @param {ColorTypes} color
   * @param {GetColorOptions} [options]
   * @returns {PlainColorObject}
   */
  /**
   * @overload
   * @param {ColorTypes[]} color
   * @param {GetColorOptions} [options]
   * @returns {PlainColorObject[]}
   */
  function getColor (color, options) {
  	if (Array.isArray(color)) {
  		return color.map(c => getColor(c, options));
  	}

  	if (!color) {
  		throw new TypeError("Empty color reference");
  	}

  	if (isString(color)) {
  		color = parse$4(color, options);
  	}

  	// Object fixup
  	let space = color.space || color.spaceId;

  	if (typeof space === "string") {
  		// Convert string id to color space object
  		color.space = ColorSpace.get(space);
  	}

  	if (color.alpha === undefined) {
  		color.alpha = 1;
  	}

  	return color;
  }

  /**
   * @packageDocumentation
   * Defines the class and other types related to creating color spaces.
   * For the builtin color spaces, see the `spaces` module.
   */

  const ε$3 = 0.000075;

  /**
   * Class to represent a color space
   */
  class ColorSpace {
  	constructor (options) {
  		this.id = options.id;
  		this.name = options.name;
  		this.base = options.base ? ColorSpace.get(options.base) : null;
  		this.aliases = options.aliases;

  		if (this.base) {
  			this.fromBase = options.fromBase;
  			this.toBase = options.toBase;
  		}

  		// Coordinate metadata

  		let coords = options.coords ?? this.base.coords;

  		for (let name in coords) {
  			if (!("name" in coords[name])) {
  				coords[name].name = name;
  			}
  		}
  		this.coords = coords;

  		// White point

  		let white = options.white ?? this.base.white ?? "D65";
  		this.white = getWhite(white);

  		// Sort out formats

  		this.formats = options.formats ?? {};

  		for (let name in this.formats) {
  			let format = this.formats[name];
  			format.type ||= "function";
  			format.name ||= name;
  		}

  		if (!this.formats.color?.id) {
  			this.formats.color = {
  				...(this.formats.color ?? {}),
  				id: options.cssId || this.id,
  			};
  		}

  		// Gamut space

  		if (options.gamutSpace) {
  			// Gamut space explicitly specified
  			this.gamutSpace =
  				options.gamutSpace === "self" ? this : ColorSpace.get(options.gamutSpace);
  		}
  		else {
  			// No gamut space specified, calculate a sensible default
  			if (this.isPolar) {
  				// Do not check gamut through polar coordinates
  				this.gamutSpace = this.base;
  			}
  			else {
  				this.gamutSpace = this;
  			}
  		}

  		// Optimize inGamut for unbounded spaces
  		if (this.gamutSpace.isUnbounded) {
  			this.inGamut = (coords, options) => {
  				return true;
  			};
  		}

  		// Other stuff
  		this.referred = options.referred;

  		// Compute ancestors and store them, since they will never change
  		Object.defineProperty(this, "path", {
  			value: getPath$1(this).reverse(),
  			writable: false,
  			enumerable: true,
  			configurable: true,
  		});

  		hooks.run("colorspace-init-end", this);
  	}

  	inGamut (coords, { epsilon = ε$3 } = {}) {
  		if (!this.equals(this.gamutSpace)) {
  			coords = this.to(this.gamutSpace, coords);
  			return this.gamutSpace.inGamut(coords, { epsilon });
  		}

  		let coordMeta = Object.values(this.coords);

  		return coords.every((c, i) => {
  			let meta = coordMeta[i];

  			if (meta.type !== "angle" && meta.range) {
  				if (isNone(c)) {
  					// NaN is always in gamut
  					return true;
  				}

  				let [min, max] = meta.range;
  				return (
  					(min === undefined || c >= min - epsilon) &&
  					(max === undefined || c <= max + epsilon)
  				);
  			}

  			return true;
  		});
  	}

  	get isUnbounded () {
  		return Object.values(this.coords).every(coord => !("range" in coord));
  	}

  	get cssId () {
  		return this.formats?.color?.id || this.id;
  	}

  	get isPolar () {
  		for (let id in this.coords) {
  			if (this.coords[id].type === "angle") {
  				return true;
  			}
  		}

  		return false;
  	}

  	/**
  	 * Lookup a format in this color space
  	 * @param {string | object | Format} format - Format id if string. If object, it's converted to a `Format` object and returned.
  	 * @returns {Format}
  	 */
  	getFormat (format) {
  		if (!format) {
  			return null;
  		}

  		if (format === "default") {
  			format = Object.values(this.formats)[0];
  		}
  		else if (typeof format === "string") {
  			format = this.formats[format];
  		}

  		let ret = Format.get(format, this);

  		if (ret !== format && format.name in this.formats) {
  			// Update the format we have on file so we can find it more quickly next time
  			this.formats[format.name] = ret;
  		}

  		return ret;
  	}

  	/**
  	 * Check if this color space is the same as another color space reference.
  	 * Allows proxying color space objects and comparing color spaces with ids.
  	 * @param {string | ColorSpace} space ColorSpace object or id to compare to
  	 * @returns {boolean}
  	 */
  	equals (space) {
  		if (!space) {
  			return false;
  		}

  		return this === space || this.id === space || this.id === space.id;
  	}

  	to (space, coords) {
  		if (arguments.length === 1) {
  			const color = getColor(space);
  			[space, coords] = [color.space, color.coords];
  		}

  		space = ColorSpace.get(space);

  		if (this.equals(space)) {
  			// Same space, no change needed
  			return coords;
  		}

  		// Convert NaN to 0, which seems to be valid in every coordinate of every color space
  		coords = coords.map(c => (isNone(c) ? 0 : c));

  		// Find connection space = lowest common ancestor in the base tree
  		let myPath = this.path;
  		let otherPath = space.path;

  		let connectionSpace, connectionSpaceIndex;

  		for (let i = 0; i < myPath.length; i++) {
  			if (myPath[i].equals(otherPath[i])) {
  				connectionSpace = myPath[i];
  				connectionSpaceIndex = i;
  			}
  			else {
  				break;
  			}
  		}

  		if (!connectionSpace) {
  			// This should never happen
  			throw new Error(
  				`Cannot convert between color spaces ${this} and ${space}: no connection space was found`,
  			);
  		}

  		// Go up from current space to connection space
  		for (let i = myPath.length - 1; i > connectionSpaceIndex; i--) {
  			coords = myPath[i].toBase(coords);
  		}

  		// Go down from connection space to target space
  		for (let i = connectionSpaceIndex + 1; i < otherPath.length; i++) {
  			coords = otherPath[i].fromBase(coords);
  		}

  		return coords;
  	}

  	from (space, coords) {
  		if (arguments.length === 1) {
  			const color = getColor(space);
  			[space, coords] = [color.space, color.coords];
  		}

  		space = ColorSpace.get(space);

  		return space.to(this, coords);
  	}

  	toString () {
  		return `${this.name} (${this.id})`;
  	}

  	getMinCoords () {
  		let ret = [];

  		for (let id in this.coords) {
  			let meta = this.coords[id];
  			let range = meta.range || meta.refRange;
  			ret.push(range?.min ?? 0);
  		}

  		return ret;
  	}

  	static registry = {};

  	// Returns array of unique color spaces
  	static get all () {
  		return [...new Set(Object.values(ColorSpace.registry))];
  	}

  	static register (id, space) {
  		if (arguments.length === 1) {
  			space = arguments[0];
  			id = space.id;
  		}

  		space = this.get(space);

  		if (this.registry[id] && this.registry[id] !== space) {
  			throw new Error(`Duplicate color space registration: '${id}'`);
  		}
  		this.registry[id] = space;

  		// Register aliases when called without an explicit ID.
  		if (arguments.length === 1 && space.aliases) {
  			for (let alias of space.aliases) {
  				this.register(alias, space);
  			}
  		}

  		return space;
  	}

  	/**
  	 * Lookup ColorSpace object by name
  	 * @param {ColorSpace | string} name
  	 */
  	static get (space, ...alternatives) {
  		if (!space || isInstance(space, this)) {
  			return space;
  		}

  		let argType = type$1(space);

  		if (argType === "string") {
  			// It's a color space id
  			let ret = ColorSpace.registry[space.toLowerCase()];

  			if (!ret) {
  				throw new TypeError(`No color space found with id = "${space}"`);
  			}

  			return ret;
  		}

  		if (alternatives.length) {
  			return ColorSpace.get(...alternatives);
  		}

  		throw new TypeError(`${space} is not a valid color space`);
  	}

  	/**
  	 * Look up all color spaces for a format that matches certain criteria
  	 * @param {object | string} filters
  	 * @param {Array<ColorSpace>} [spaces=ColorSpace.all]
  	 * @returns {Format | null}
  	 */
  	static findFormat (filters, spaces = ColorSpace.all) {
  		if (!filters) {
  			return null;
  		}

  		if (typeof filters === "string") {
  			filters = { name: filters };
  		}

  		for (let space of spaces) {
  			for (let [name, format] of Object.entries(space.formats)) {
  				format.name ??= name;
  				format.type ??= "function";

  				let matches =
  					(!filters.name || format.name === filters.name) &&
  					(!filters.type || format.type === filters.type);

  				if (filters.id) {
  					let ids = format.ids || [format.id];
  					let filterIds = Array.isArray(filters.id) ? filters.id : [filters.id];
  					matches &&= filterIds.some(id => ids.includes(id));
  				}

  				if (matches) {
  					let ret = Format.get(format, space);

  					if (ret !== format) {
  						space.formats[format.name] = ret;
  					}

  					return ret;
  				}
  			}
  		}

  		return null;
  	}

  	/**
  	 * Get metadata about a coordinate of a color space
  	 *
  	 * @static
  	 * @param {Array | string} ref
  	 * @param {ColorSpace | string} [workingSpace]
  	 * @return {Object}
  	 */
  	static resolveCoord (ref, workingSpace) {
  		let coordType = type$1(ref);
  		let space, coord;

  		if (coordType === "string") {
  			if (ref.includes(".")) {
  				// Absolute coordinate
  				[space, coord] = ref.split(".");
  			}
  			else {
  				// Relative coordinate
  				[space, coord] = [, ref];
  			}
  		}
  		else if (Array.isArray(ref)) {
  			[space, coord] = ref;
  		}
  		else {
  			// Object
  			space = ref.space;
  			coord = ref.coordId;
  		}

  		space = ColorSpace.get(space);

  		if (!space) {
  			space = workingSpace;
  		}

  		if (!space) {
  			throw new TypeError(
  				`Cannot resolve coordinate reference ${ref}: No color space specified and relative references are not allowed here`,
  			);
  		}

  		coordType = type$1(coord);

  		if (coordType === "number" || (coordType === "string" && coord >= 0)) {
  			// Resolve numerical coord
  			let meta = Object.entries(space.coords)[coord];

  			if (meta) {
  				return { space, id: meta[0], index: coord, ...meta[1] };
  			}
  		}

  		space = ColorSpace.get(space);

  		let normalizedCoord = coord.toLowerCase();

  		let i = 0;
  		for (let id in space.coords) {
  			let meta = space.coords[id];

  			if (
  				id.toLowerCase() === normalizedCoord ||
  				meta.name?.toLowerCase() === normalizedCoord
  			) {
  				return { space, id, index: i, ...meta };
  			}

  			i++;
  		}

  		throw new TypeError(
  			`No "${coord}" coordinate found in ${space.name}. Its coordinates are: ${Object.keys(space.coords).join(", ")}`,
  		);
  	}

  	static DEFAULT_FORMAT = {
  		type: "functions",
  		name: "color",
  	};
  }

  function getPath$1 (space) {
  	let ret = [space];

  	for (let s = space; (s = s.base); ) {
  		ret.push(s);
  	}

  	return ret;
  }

  var xyz_d65 = new ColorSpace({
  	id: "xyz-d65",
  	name: "XYZ D65",
  	coords: {
  		x: {
  			refRange: [0, 1],
  			name: "X",
  		},
  		y: {
  			refRange: [0, 1],
  			name: "Y",
  		},
  		z: {
  			refRange: [0, 1],
  			name: "Z",
  		},
  	},
  	white: "D65",
  	formats: {
  		color: {
  			ids: ["xyz-d65", "xyz"],
  		},
  	},
  	aliases: ["xyz"],
  });

  // Type re-exports
  /** @typedef {import("./types.js").RGBOptions} RGBOptions */

  /** Convenience class for RGB color spaces */
  class RGBColorSpace extends ColorSpace {
  	/**
  	 * Creates a new RGB ColorSpace.
  	 * If coords are not specified, they will use the default RGB coords.
  	 * Instead of `fromBase()` and `toBase()` functions,
  	 * you can specify to/from XYZ matrices and have `toBase()` and `fromBase()` automatically generated.
  	 * @param {RGBOptions} options
  	 */
  	constructor (options) {
  		if (!options.coords) {
  			options.coords = {
  				r: {
  					range: [0, 1],
  					name: "Red",
  				},
  				g: {
  					range: [0, 1],
  					name: "Green",
  				},
  				b: {
  					range: [0, 1],
  					name: "Blue",
  				},
  			};
  		}

  		if (!options.base) {
  			options.base = xyz_d65;
  		}

  		if (options.toXYZ_M && options.fromXYZ_M) {
  			options.toBase ??= rgb => {
  				let xyz = multiply_v3_m3x3(rgb, options.toXYZ_M);

  				if (this.white !== this.base.white) {
  					// Perform chromatic adaptation
  					xyz = adapt$1(this.white, this.base.white, xyz);
  				}

  				return xyz;
  			};

  			options.fromBase ??= xyz => {
  				xyz = adapt$1(this.base.white, this.white, xyz);
  				return multiply_v3_m3x3(xyz, options.fromXYZ_M);
  			};
  		}

  		options.referred ??= "display";

  		super(options);
  	}
  }

  /** @import { ColorTypes, Coords } from "./types.js" */

  /**
   * Options for {@link getAll}
   * @typedef GetAllOptions
   * @property {string | ColorSpace | undefined} [space]
   * The color space to convert to. Defaults to the color's current space
   * @property {number | undefined} [precision]
   * The number of significant digits to round the coordinates to
   */

  /**
   * Get the coordinates of a color in any color space
   * @overload
   * @param {ColorTypes} color
   * @param {string | ColorSpace} [options=color.space] The color space to convert to. Defaults to the color's current space
   * @returns {Coords} The color coordinates in the given color space
   */
  /**
   * @overload
   * @param {ColorTypes} color
   * @param {GetAllOptions} [options]
   * @returns {Coords} The color coordinates in the given color space
   */
  function getAll (color, options) {
  	color = getColor(color);

  	let space = ColorSpace.get(options, options?.space);
  	let precision = options?.precision;

  	let coords;
  	if (!space || color.space.equals(space)) {
  		// No conversion needed
  		coords = color.coords.slice();
  	}
  	else {
  		coords = space.from(color);
  	}

  	return precision === undefined ? coords : coords.map(coord => toPrecision(coord, precision));
  }

  /** @import { ColorTypes, Ref } from "./types.js" */

  /**
   * @param {ColorTypes} color
   * @param {Ref} prop
   * @returns {number}
   */
  function get$1 (color, prop) {
  	color = getColor(color);

  	if (prop === "alpha") {
  		return color.alpha ?? 1;
  	}

  	let { space, index } = ColorSpace.resolveCoord(prop, color.space);
  	let coords = getAll(color, space);
  	return coords[index];
  }

  /** @import { ColorTypes, Coords, PlainColorObject } from "./types.js" */

  /**
   * Set all coordinates of a color at once, in its own color space or another.
   * Modifies the color in place.
   * @overload
   * @param {ColorTypes} color
   * @param {Coords} coords Array of coordinates
   * @param {number} [alpha]
   * @returns {PlainColorObject}
   */
  /**
   * @overload
   * @param {ColorTypes} color
   * @param {string | ColorSpace} space The color space of the provided coordinates.
   * @param {Coords} coords Array of coordinates
   * @param {number} [alpha]
   * @returns {PlainColorObject}
   */
  function setAll (color, space, coords, alpha) {
  	color = getColor(color);

  	if (Array.isArray(space)) {
  		// Space is omitted
  		[space, coords, alpha] = [color.space, space, coords];
  	}

  	space = ColorSpace.get(space); // Make sure we have a ColorSpace object
  	color.coords = space === color.space ? coords.slice() : space.to(color.space, coords);

  	if (alpha !== undefined) {
  		color.alpha = alpha;
  	}

  	return color;
  }

  /** @type {"color"} */
  setAll.returns = "color";

  /** @import { ColorTypes, PlainColorObject, Ref } from "./types.js" */

  /**
   * Set properties and return current instance
   * @overload
   * @param {ColorTypes} color
   * @param {Ref} prop
   * @param {number | ((coord: number) => number)} value
   * @returns {PlainColorObject}
   */
  /**
   * @overload
   * @param {ColorTypes} color
   * @param {Record<string, number | ((coord: number) => number)>} props
   * @returns {PlainColorObject}
   */
  function set (color, prop, value) {
  	color = getColor(color);

  	if (arguments.length === 2 && type$1(arguments[1]) === "object") {
  		// Argument is an object literal
  		let object = arguments[1];
  		for (let p in object) {
  			set(color, p, object[p]);
  		}
  	}
  	else {
  		if (typeof value === "function") {
  			value = value(get$1(color, prop));
  		}

  		if (prop === "alpha") {
  			color.alpha = value;
  		}
  		else {
  			let { space, index } = ColorSpace.resolveCoord(prop, color.space);
  			let coords = getAll(color, space);
  			coords[index] = value;
  			setAll(color, space, coords);
  		}
  	}

  	return color;
  }

  /** @type {"color"} */
  set.returns = "color";

  var XYZ_D50 = new ColorSpace({
  	id: "xyz-d50",
  	name: "XYZ D50",
  	white: "D50",
  	base: xyz_d65,
  	fromBase: coords => adapt$1(xyz_d65.white, "D50", coords),
  	toBase: coords => adapt$1("D50", xyz_d65.white, coords),
  });

  // κ * ε  = 2^3 = 8
  const ε$2 = 216 / 24389; // 6^3/29^3 == (24/116)^3
  const ε3 = 24 / 116;
  const κ$1 = 24389 / 27; // 29^3/3^3

  let white$2 = WHITES.D50;

  var Lab = new ColorSpace({
  	id: "lab",
  	name: "Lab",
  	coords: {
  		l: {
  			refRange: [0, 100],
  			name: "Lightness",
  		},
  		a: {
  			refRange: [-125, 125],
  		},
  		b: {
  			refRange: [-125, 125],
  		},
  	},

  	// Assuming XYZ is relative to D50, convert to CIE Lab
  	// from CIE standard, which now defines these as a rational fraction
  	white: white$2,

  	base: XYZ_D50,
  	// Convert D50-adapted XYX to Lab
  	// CIE 15.3:2004 section 8.2.1.1
  	fromBase (XYZ) {
  		// XYZ scaled relative to reference white
  		let xyz = XYZ.map((value, i) => value / white$2[i]);
  		let f = xyz.map(value => (value > ε$2 ? Math.cbrt(value) : (κ$1 * value + 16) / 116));

  		let L = 116 * f[1] - 16;
  		let a = 500 * (f[0] - f[1]);
  		let b = 200 * (f[1] - f[2]);

  		return [L, a, b];
  	},
  	// Convert Lab to D50-adapted XYZ
  	// Same result as CIE 15.3:2004 Appendix D although the derivation is different
  	// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
  	toBase (Lab) {
  		// compute f, starting with the luminance-related term
  		let [L, a, b] = Lab;
  		let f = [];
  		f[1] = (L + 16) / 116;
  		f[0] = a / 500 + f[1];
  		f[2] = f[1] - b / 200;

  		// compute xyz
  		// prettier-ignore
  		let xyz = [
  			f[0]   > ε3 ? Math.pow(f[0], 3)                : (116 * f[0] - 16) / κ$1,
  			Lab[0] > 8  ? Math.pow((Lab[0] + 16) / 116, 3) : Lab[0] / κ$1,
  			f[2]   > ε3 ? Math.pow(f[2], 3)                : (116 * f[2] - 16) / κ$1,
  		];

  		// Compute XYZ by scaling xyz by reference white
  		return xyz.map((value, i) => value * white$2[i]);
  	},

  	formats: {
  		lab: {
  			coords: [
  				"<percentage> | <number>",
  				"<number> | <percentage>",
  				"<number> | <percentage>",
  			],
  		},
  	},
  });

  /**
   * Constrain an angle to 360 degrees
   * @param {number} angle
   * @returns {number}
   */
  function constrain$1 (angle) {
  	if (typeof angle !== "number") {
  		return angle;
  	}

  	return ((angle % 360) + 360) % 360;
  }

  /**
   * @param {"raw" | "increasing" | "decreasing" | "longer" | "shorter"} arc
   * @param {[number, number]} angles
   * @returns {[number, number]}
   */
  function adjust (arc, angles) {
  	let [a1, a2] = angles;

  	let none1 = isNone(a1);
  	let none2 = isNone(a2);

  	if (none1 && none2) {
  		return [a1, a2];
  	}
  	else if (none1) {
  		a1 = a2;
  	}
  	else if (none2) {
  		a2 = a1;
  	}

  	if (arc === "raw") {
  		return angles;
  	}

  	a1 = constrain$1(a1);
  	a2 = constrain$1(a2);

  	let angleDiff = a2 - a1;

  	if (arc === "increasing") {
  		if (angleDiff < 0) {
  			a2 += 360;
  		}
  	}
  	else if (arc === "decreasing") {
  		if (angleDiff > 0) {
  			a1 += 360;
  		}
  	}
  	else if (arc === "longer") {
  		if (-180 < angleDiff && angleDiff < 180) {
  			if (angleDiff > 0) {
  				a1 += 360;
  			}
  			else {
  				a2 += 360;
  			}
  		}
  	}
  	else if (arc === "shorter") {
  		if (angleDiff > 180) {
  			a1 += 360;
  		}
  		else if (angleDiff < -180) {
  			a2 += 360;
  		}
  	}

  	return [a1, a2];
  }

  var LCHSpace = new ColorSpace({
  	id: "lch",
  	name: "LCH",
  	coords: {
  		l: {
  			refRange: [0, 100],
  			name: "Lightness",
  		},
  		c: {
  			refRange: [0, 150],
  			name: "Chroma",
  		},
  		h: {
  			refRange: [0, 360],
  			type: "angle",
  			name: "Hue",
  		},
  	},

  	base: Lab,
  	fromBase (Lab) {
  		// These methods are used for other polar forms as well, so we can't hardcode the ε
  		if (this.ε === undefined) {
  			// @ts-expect-error Property 'coords' does not exist on type 'string | ColorSpace'
  			let range = Object.values(this.base.coords)[1].refRange;
  			let extent = range[1] - range[0];
  			this.ε = extent / 100000;
  		}

  		// Convert to polar form
  		let [L, a, b] = Lab;
  		let isAchromatic = Math.abs(a) < this.ε && Math.abs(b) < this.ε;
  		let h = isAchromatic ? null : constrain$1((Math.atan2(b, a) * 180) / Math.PI);
  		let C = isAchromatic ? 0 : Math.sqrt(a ** 2 + b ** 2);

  		return [L, C, h];
  	},
  	toBase (lch) {
  		// Convert from polar form
  		let [L, C, h] = lch;
  		let a = null,
  			b = null;

  		if (!isNone(h)) {
  			C = C < 0 ? 0 : C; // Clamp negative Chroma
  			a = C * Math.cos((h * Math.PI) / 180);
  			b = C * Math.sin((h * Math.PI) / 180);
  		}

  		return [L, a, b];
  	},

  	formats: {
  		lch: {
  			coords: ["<percentage> | <number>", "<number> | <percentage>", "<number> | <angle>"],
  		},
  	},
  });

  // deltaE2000 is a statistically significant improvement
  // and is recommended by the CIE and Idealliance
  // especially for color differences less than 10 deltaE76
  // but is wicked complicated
  // and many implementations have small errors!
  // DeltaE2000 is also discontinuous; in case this
  // matters to you, use deltaECMC instead.

  const Gfactor = 25 ** 7;
  const π$1 = Math.PI;
  const r2d = 180 / π$1;
  const d2r$1 = π$1 / 180;

  function pow7 (x) {
  	// Faster than x ** 7 or Math.pow(x, 7)

  	const x2 = x * x;
  	const x7 = x2 * x2 * x2 * x;

  	return x7;
  }

  /**
   * @param {import("../types.js").ColorTypes} color
   * @param {import("../types.js").ColorTypes} sample
   * @param {{ kL?: number | undefined; kC?: number | undefined; kH?: number | undefined }} options
   * @returns {number}
   */
  function deltaE2000 (color, sample, { kL = 1, kC = 1, kH = 1 } = {}) {
  	[color, sample] = getColor([color, sample]);

  	// Given this color as the reference
  	// and the function parameter as the sample,
  	// calculate deltaE 2000.

  	// This implementation assumes the parametric
  	// weighting factors kL, kC and kH
  	// for the influence of viewing conditions
  	// are all 1, as sadly seems typical.
  	// kL should be increased for lightness texture or noise
  	// and kC increased for chroma noise

  	let [L1, a1, b1] = Lab.from(color);
  	let C1 = LCHSpace.from(Lab, [L1, a1, b1])[1];
  	let [L2, a2, b2] = Lab.from(sample);
  	let C2 = LCHSpace.from(Lab, [L2, a2, b2])[1];

  	// Check for negative Chroma,
  	// which might happen through
  	// direct user input of LCH values

  	if (C1 < 0) {
  		C1 = 0;
  	}
  	if (C2 < 0) {
  		C2 = 0;
  	}

  	let Cbar = (C1 + C2) / 2; // mean Chroma

  	// calculate a-axis asymmetry factor from mean Chroma
  	// this turns JND ellipses for near-neutral colors back into circles
  	let C7 = pow7(Cbar);

  	let G = 0.5 * (1 - Math.sqrt(C7 / (C7 + Gfactor)));

  	// scale a axes by asymmetry factor
  	// this by the way is why there is no Lab2000 colorspace
  	let adash1 = (1 + G) * a1;
  	let adash2 = (1 + G) * a2;

  	// calculate new Chroma from scaled a and original b axes
  	let Cdash1 = Math.sqrt(adash1 ** 2 + b1 ** 2);
  	let Cdash2 = Math.sqrt(adash2 ** 2 + b2 ** 2);

  	// calculate new hues, with zero hue for true neutrals
  	// and in degrees, not radians

  	let h1 = adash1 === 0 && b1 === 0 ? 0 : Math.atan2(b1, adash1);
  	let h2 = adash2 === 0 && b2 === 0 ? 0 : Math.atan2(b2, adash2);

  	if (h1 < 0) {
  		h1 += 2 * π$1;
  	}
  	if (h2 < 0) {
  		h2 += 2 * π$1;
  	}

  	h1 *= r2d;
  	h2 *= r2d;

  	// Lightness and Chroma differences; sign matters
  	let ΔL = L2 - L1;
  	let ΔC = Cdash2 - Cdash1;

  	// Hue difference, getting the sign correct
  	let hdiff = h2 - h1;
  	let hsum = h1 + h2;
  	let habs = Math.abs(hdiff);
  	let Δh;

  	if (Cdash1 * Cdash2 === 0) {
  		Δh = 0;
  	}
  	else if (habs <= 180) {
  		Δh = hdiff;
  	}
  	else if (hdiff > 180) {
  		Δh = hdiff - 360;
  	}
  	else if (hdiff < -180) {
  		Δh = hdiff + 360;
  	}
  	else {
  		defaults$1.warn("the unthinkable has happened");
  	}

  	// weighted Hue difference, more for larger Chroma
  	let ΔH = 2 * Math.sqrt(Cdash2 * Cdash1) * Math.sin((Δh * d2r$1) / 2);

  	// calculate mean Lightness and Chroma
  	let Ldash = (L1 + L2) / 2;
  	let Cdash = (Cdash1 + Cdash2) / 2;
  	let Cdash7 = pow7(Cdash);

  	// Compensate for non-linearity in the blue region of Lab.
  	// Four possibilities for hue weighting factor,
  	// depending on the angles, to get the correct sign
  	let hdash;
  	if (Cdash1 * Cdash2 === 0) {
  		hdash = hsum; // which should be zero
  	}
  	else if (habs <= 180) {
  		hdash = hsum / 2;
  	}
  	else if (hsum < 360) {
  		hdash = (hsum + 360) / 2;
  	}
  	else {
  		hdash = (hsum - 360) / 2;
  	}

  	// positional corrections to the lack of uniformity of CIELAB
  	// These are all trying to make JND ellipsoids more like spheres

  	// SL Lightness crispening factor
  	// a background with L=50 is assumed
  	let lsq = (Ldash - 50) ** 2;
  	let SL = 1 + (0.015 * lsq) / Math.sqrt(20 + lsq);

  	// SC Chroma factor, similar to those in CMC and deltaE 94 formulae
  	let SC = 1 + 0.045 * Cdash;

  	// Cross term T for blue non-linearity
  	let T = 1;
  	T -= 0.17 * Math.cos((hdash - 30) * d2r$1);
  	T += 0.24 * Math.cos(2 * hdash * d2r$1);
  	T += 0.32 * Math.cos((3 * hdash + 6) * d2r$1);
  	T -= 0.2 * Math.cos((4 * hdash - 63) * d2r$1);

  	// SH Hue factor depends on Chroma,
  	// as well as adjusted hue angle like deltaE94.
  	let SH = 1 + 0.015 * Cdash * T;

  	// RT Hue rotation term compensates for rotation of JND ellipses
  	// and Munsell constant hue lines
  	// in the medium-high Chroma blue region
  	// (Hue 225 to 315)
  	let Δθ = 30 * Math.exp(-1 * ((hdash - 275) / 25) ** 2);
  	let RC = 2 * Math.sqrt(Cdash7 / (Cdash7 + Gfactor));
  	let RT = -1 * Math.sin(2 * Δθ * d2r$1) * RC;

  	// Finally calculate the deltaE, term by term as root sume of squares
  	let dE = (ΔL / (kL * SL)) ** 2;
  	dE += (ΔC / (kC * SC)) ** 2;
  	dE += (ΔH / (kH * SH)) ** 2;
  	dE += RT * (ΔC / (kC * SC)) * (ΔH / (kH * SH));
  	return Math.sqrt(dE);
  	// Yay!!!
  }

  /** @import { Matrix3x3 } from "../types.js" */

  // Recalculated for consistent reference white
  // see https://github.com/w3c/csswg-drafts/issues/6642#issuecomment-943521484
  /** @type {Matrix3x3} */
  // prettier-ignore
  const XYZtoLMS_M$1 = [
  	[ 0.8190224379967030, 0.3619062600528904, -0.1288737815209879 ],
  	[ 0.0329836539323885, 0.9292868615863434,  0.0361446663506424 ],
  	[ 0.0481771893596242, 0.2642395317527308,  0.6335478284694309 ],
  ];
  // inverse of XYZtoLMS_M
  /** @type {Matrix3x3} */
  // prettier-ignore
  const LMStoXYZ_M$1 = [
  	[  1.2268798758459243, -0.5578149944602171,  0.2813910456659647 ],
  	[ -0.0405757452148008,  1.1122868032803170, -0.0717110580655164 ],
  	[ -0.0763729366746601, -0.4214933324022432,  1.5869240198367816 ],
  ];
  /** @type {Matrix3x3} */
  // prettier-ignore
  const LMStoLab_M = [
  	[ 0.2104542683093140,  0.7936177747023054, -0.0040720430116193 ],
  	[ 1.9779985324311684, -2.42859224204858,  0.4505937096174110 ],
  	[ 0.0259040424655478,  0.7827717124575296, -0.8086757549230774 ],
  ];
  // LMStoIab_M inverted
  /** @type {Matrix3x3} */
  // prettier-ignore
  const LabtoLMS_M = [
  	[ 1.0000000000000000,  0.3963377773761749,  0.2158037573099136 ],
  	[ 1.0000000000000000, -0.1055613458156586, -0.0638541728258133 ],
  	[ 1.0000000000000000, -0.0894841775298119, -1.2914855480194092 ],
  ];

  var OKLab = new ColorSpace({
  	id: "oklab",
  	name: "Oklab",
  	coords: {
  		l: {
  			refRange: [0, 1],
  			name: "Lightness",
  		},
  		a: {
  			refRange: [-0.4, 0.4],
  		},
  		b: {
  			refRange: [-0.4, 0.4],
  		},
  	},

  	// Note that XYZ is relative to D65
  	white: "D65",
  	base: xyz_d65,
  	fromBase (XYZ) {
  		// move to LMS cone domain
  		let LMS = multiply_v3_m3x3(XYZ, XYZtoLMS_M$1);

  		// non-linearity
  		LMS[0] = Math.cbrt(LMS[0]);
  		LMS[1] = Math.cbrt(LMS[1]);
  		LMS[2] = Math.cbrt(LMS[2]);

  		return multiply_v3_m3x3(LMS, LMStoLab_M, LMS);
  	},
  	toBase (OKLab) {
  		// move to LMS cone domain
  		let LMSg = multiply_v3_m3x3(OKLab, LabtoLMS_M);

  		// restore linearity
  		LMSg[0] = LMSg[0] ** 3;
  		LMSg[1] = LMSg[1] ** 3;
  		LMSg[2] = LMSg[2] ** 3;

  		return multiply_v3_m3x3(LMSg, LMStoXYZ_M$1, LMSg);
  	},

  	formats: {
  		oklab: {
  			coords: [
  				"<percentage> | <number>",
  				"<number> | <percentage>",
  				"<number> | <percentage>",
  			],
  		},
  	},
  });

  /**
   * More accurate color-difference formulae
   * than the simple 1976 Euclidean distance in CIE Lab
   * @param {import("../types.js").ColorTypes} color
   * @param {import("../types.js").ColorTypes} sample
   * @returns {number}
   */
  function deltaEOK (color, sample) {
  	[color, sample] = getColor([color, sample]);

  	// Given this color as the reference
  	// and a sample,
  	// calculate deltaEOK, term by term as root sum of squares
  	let [L1, a1, b1] = OKLab.from(color);
  	let [L2, a2, b2] = OKLab.from(sample);
  	let ΔL = L1 - L2;
  	let Δa = a1 - a2;
  	let Δb = b1 - b2;
  	return Math.sqrt(ΔL ** 2 + Δa ** 2 + Δb ** 2);
  }

  /** @import { ColorTypes } from "./types.js" */

  const ε$1 = 0.000075;

  /**
   * Check if a color is in gamut of either its own or another color space
   * @param {ColorTypes} color
   * @param {string | ColorSpace} [space]
   * @param {{ epsilon?: number | undefined }} [param2]
   * @returns {boolean}
   */
  function inGamut (color, space, { epsilon = ε$1 } = {}) {
  	color = getColor(color);

  	if (!space) {
  		space = color.space;
  	}

  	space = ColorSpace.get(space);
  	let coords = color.coords;

  	if (space !== color.space) {
  		coords = space.from(color);
  	}

  	return space.inGamut(coords, { epsilon });
  }

  /** @import { Coords, PlainColorObject } from "./types.js" */

  /**
   * @param {PlainColorObject} color
   * @returns {PlainColorObject}
   */
  function clone$1 (color) {
  	return {
  		space: color.space,
  		coords: /** @type {Coords} */ (color.coords.slice()),
  		alpha: color.alpha,
  	};
  }

  /** @import { ColorTypes } from "./types.js" */

  /**
   * Euclidean distance of colors in an arbitrary color space
   * @param {ColorTypes} color1
   * @param {ColorTypes} color2
   * @param {string | ColorSpace} space
   * @returns {number}
   */
  function distance (color1, color2, space = "lab") {
  	space = ColorSpace.get(space);

  	// Assume getColor() is called on color in space.from()
  	let coords1 = space.from(color1);
  	let coords2 = space.from(color2);

  	return Math.sqrt(
  		coords1.reduce((acc, c1, i) => {
  			let c2 = coords2[i];
  			if (isNone(c1) || isNone(c2)) {
  				return acc;
  			}

  			return acc + (c2 - c1) ** 2;
  		}, 0),
  	);
  }

  /**
   * @param {import("../types.js").ColorTypes} color
   * @param {import("../types.js").ColorTypes} sample
   * @returns {number}
   */
  function deltaE76 (color, sample) {
  	// Assume getColor() is called in the distance function
  	return distance(color, sample, "lab");
  }

  // More accurate color-difference formulae
  // than the simple 1976 Euclidean distance in Lab

  // CMC by the Color Measurement Committee of the
  // Bradford Society of Dyeists and Colorsts, 1994.
  // Uses LCH rather than Lab,
  // with different weights for L, C and H differences
  // A nice increase in accuracy for modest increase in complexity
  const π = Math.PI;
  const d2r = π / 180;

  /**
   * @param {import("../types.js").ColorTypes} color
   * @param {import("../types.js").ColorTypes} sample
   * @param {{ l?: number | undefined; c?: number | undefined }} options
   * @returns {number}
   */
  function deltaECMC (color, sample, { l = 2, c = 1 } = {}) {
  	[color, sample] = getColor([color, sample]);

  	// Given this color as the reference
  	// and a sample,
  	// calculate deltaE CMC.

  	// This implementation assumes the parametric
  	// weighting factors l:c are 2:1
  	// which is typical for non-textile uses.

  	let [L1, a1, b1] = Lab.from(color);
  	let [, C1, H1] = LCHSpace.from(Lab, [L1, a1, b1]);
  	let [L2, a2, b2] = Lab.from(sample);
  	let C2 = LCHSpace.from(Lab, [L2, a2, b2])[1];

  	// let [L1, a1, b1] = color.getAll(lab);
  	// let C1 = color.get("lch.c");
  	// let H1 = color.get("lch.h");
  	// let [L2, a2, b2] = sample.getAll(lab);
  	// let C2 = sample.get("lch.c");

  	// Check for negative Chroma,
  	// which might happen through
  	// direct user input of LCH values

  	if (C1 < 0) {
  		C1 = 0;
  	}
  	if (C2 < 0) {
  		C2 = 0;
  	}

  	// we don't need H2 as ΔH is calculated from Δa, Δb and ΔC

  	// Lightness and Chroma differences
  	// These are (color - sample), unlike deltaE2000
  	let ΔL = L1 - L2;
  	let ΔC = C1 - C2;

  	let Δa = a1 - a2;
  	let Δb = b1 - b2;

  	// weighted Hue difference, less for larger Chroma difference

  	let H2 = Δa ** 2 + Δb ** 2 - ΔC ** 2;
  	// due to roundoff error it is possible that, for zero a and b,
  	// ΔC > Δa + Δb is 0, resulting in attempting
  	// to take the square root of a negative number

  	// trying instead the equation from Industrial Color Physics
  	// By Georg A. Klein

  	// let ΔH = ((a1 * b2) - (a2 * b1)) / Math.sqrt(0.5 * ((C2 * C1) + (a2 * a1) + (b2 * b1)));
  	// console.log({ΔH});
  	// This gives the same result to 12 decimal places
  	// except it sometimes NaNs when trying to root a negative number

  	// let ΔH = Math.sqrt(H2); we never actually use the root, it gets squared again!!

  	// positional corrections to the lack of uniformity of CIELAB
  	// These are all trying to make JND ellipsoids more like spheres

  	// SL Lightness crispening factor, depends entirely on L1 not L2
  	let SL = 0.511; // linear portion of the Y to L transfer function
  	if (L1 >= 16) {
  		// cubic portion
  		SL = (0.040975 * L1) / (1 + 0.01765 * L1);
  	}

  	// SC Chroma factor
  	let SC = (0.0638 * C1) / (1 + 0.0131 * C1) + 0.638;

  	// Cross term T for blue non-linearity
  	let T;
  	if (isNone(H1)) {
  		H1 = 0;
  	}

  	if (H1 >= 164 && H1 <= 345) {
  		T = 0.56 + Math.abs(0.2 * Math.cos((H1 + 168) * d2r));
  	}
  	else {
  		T = 0.36 + Math.abs(0.4 * Math.cos((H1 + 35) * d2r));
  	}
  	// console.log({T});

  	// SH Hue factor also depends on C1,
  	let C4 = Math.pow(C1, 4);
  	let F = Math.sqrt(C4 / (C4 + 1900));
  	let SH = SC * (F * T + 1 - F);

  	// Finally calculate the deltaE, term by term as root sume of squares
  	let dE = (ΔL / (l * SL)) ** 2;
  	dE += (ΔC / (c * SC)) ** 2;
  	dE += H2 / SH ** 2;
  	// dE += (ΔH / SH)  ** 2;
  	return Math.sqrt(dE);
  	// Yay!!!
  }

  const Yw = 203; // absolute luminance of media white

  var XYZ_Abs_D65 = new ColorSpace({
  	// Absolute CIE XYZ, with a D65 whitepoint,
  	// as used in most HDR colorspaces as a starting point.
  	// SDR spaces are converted per BT.2048
  	// so that diffuse, media white is 203 cd/m²
  	id: "xyz-abs-d65",
  	cssId: "--xyz-abs-d65",
  	name: "Absolute XYZ D65",
  	coords: {
  		x: {
  			refRange: [0, 9504.7],
  			name: "Xa",
  		},
  		y: {
  			refRange: [0, 10000],
  			name: "Ya",
  		},
  		z: {
  			refRange: [0, 10888.3],
  			name: "Za",
  		},
  	},

  	base: xyz_d65,
  	fromBase (XYZ) {
  		// Make XYZ absolute, not relative to media white
  		// Maximum luminance in PQ is 10,000 cd/m²
  		// Relative XYZ has Y=1 for media white
  		return XYZ.map(v => v * Yw);
  	},
  	toBase (AbsXYZ) {
  		// Convert to media-white relative XYZ
  		return AbsXYZ.map(v => v / Yw);
  	},
  });

  /** @import { Matrix3x3, Vector3 } from "../types.js" */

  const b = 1.15;
  const g = 0.66;
  const n = 2610 / 2 ** 14;
  const ninv = 2 ** 14 / 2610;
  const c1$1 = 3424 / 2 ** 12;
  const c2$1 = 2413 / 2 ** 7;
  const c3$1 = 2392 / 2 ** 7;
  const p = (1.7 * 2523) / 2 ** 5;
  const pinv = 2 ** 5 / (1.7 * 2523);
  const d = -0.56;
  const d0 = 1.6295499532821566e-11;

  /** @type {Matrix3x3} */
  // prettier-ignore
  const XYZtoCone_M = [
  	[  0.41478972, 0.579999,  0.0146480 ],
  	[ -0.20151,  1.120649,  0.0531008 ],
  	[ -0.0166008,  0.264800,  0.6684799 ],
  ];
  // XYZtoCone_M inverted
  /** @type {Matrix3x3} */
  // prettier-ignore
  const ConetoXYZ_M = [
  	[  1.9242264357876067,  -1.0047923125953657,  0.037651404030618   ],
  	[  0.35031676209499907,  0.7264811939316552, -0.06538442294808501 ],
  	[ -0.09098281098284752, -0.3127282905230739,  1.5227665613052603  ],
  ];
  /** @type {Matrix3x3} */
  // prettier-ignore
  const ConetoIab_M = [
  	[  0.5,       0.5,       0        ],
  	[  3.524000, -4.066708,  0.542708 ],
  	[  0.199076,  1.096799, -1.295875 ],
  ];
  // ConetoIab_M inverted
  /** @type {Matrix3x3} */
  // prettier-ignore
  const IabtoCone_M = [
  	[ 1,                   0.13860504327153927,   0.05804731615611883 ],
  	[ 1,                  -0.1386050432715393,   -0.058047316156118904 ],
  	[ 1,                  -0.09601924202631895,  -0.811891896056039  ],
  ];

  var Jzazbz = new ColorSpace({
  	id: "jzazbz",
  	name: "Jzazbz",
  	coords: {
  		jz: {
  			refRange: [0, 1],
  			name: "Jz",
  		},
  		az: {
  			refRange: [-0.21, 0.21],
  		},
  		bz: {
  			refRange: [-0.21, 0.21],
  		},
  	},

  	base: XYZ_Abs_D65,
  	fromBase (XYZ) {
  		// First make XYZ absolute, not relative to media white
  		// Maximum luminance in PQ is 10,000 cd/m²
  		// Relative XYZ has Y=1 for media white
  		// BT.2048 says media white Y=203 at PQ 58

  		let [Xa, Ya, Za] = XYZ;

  		// modify X and Y to minimize blue curvature
  		let Xm = b * Xa - (b - 1) * Za;
  		let Ym = g * Ya - (g - 1) * Xa;

  		// move to LMS cone domain
  		let LMS = multiply_v3_m3x3([Xm, Ym, Za], XYZtoCone_M);

  		// PQ-encode LMS
  		let PQLMS = /** @type {Vector3} } */ (
  			LMS.map(function (val) {
  				let num = c1$1 + c2$1 * spow(val / 10000, n);
  				let denom = 1 + c3$1 * spow(val / 10000, n);

  				return spow(num / denom, p);
  			})
  		);

  		// almost there, calculate Iz az bz
  		let [Iz, az, bz] = multiply_v3_m3x3(PQLMS, ConetoIab_M);
  		// console.log({Iz, az, bz});

  		let Jz = ((1 + d) * Iz) / (1 + d * Iz) - d0;
  		return [Jz, az, bz];
  	},
  	toBase (Jzazbz) {
  		let [Jz, az, bz] = Jzazbz;
  		let Iz = (Jz + d0) / (1 + d - d * (Jz + d0));

  		// bring into LMS cone domain
  		let PQLMS = multiply_v3_m3x3([Iz, az, bz], IabtoCone_M);

  		// convert from PQ-coded to linear-light
  		let LMS = /** @type {Vector3} } */ (
  			PQLMS.map(function (val) {
  				let num = c1$1 - spow(val, pinv);
  				let denom = c3$1 * spow(val, pinv) - c2$1;
  				let x = 10000 * spow(num / denom, ninv);

  				return x; // luminance relative to diffuse white, [0, 70 or so].
  			})
  		);

  		// modified abs XYZ
  		let [Xm, Ym, Za] = multiply_v3_m3x3(LMS, ConetoXYZ_M);

  		// un-modify X and Y to get D65 XYZ, relative to media white
  		let Xa = (Xm + (b - 1) * Za) / b;
  		let Ya = (Ym + (g - 1) * Xa) / g;
  		return [Xa, Ya, Za];
  	},

  	formats: {
  		// https://drafts.csswg.org/css-color-hdr/#Jzazbz
  		jzazbz: {
  			coords: [
  				"<percentage> | <number>",
  				"<number> | <percentage>",
  				"<number> | <percentage>",
  			],
  		},
  	},
  });

  var jzczhz = new ColorSpace({
  	id: "jzczhz",
  	name: "JzCzHz",
  	coords: {
  		jz: {
  			refRange: [0, 1],
  			name: "Jz",
  		},
  		cz: {
  			refRange: [0, 0.26],
  			name: "Chroma",
  		},
  		hz: {
  			refRange: [0, 360],
  			type: "angle",
  			name: "Hue",
  		},
  	},

  	base: Jzazbz,
  	fromBase: LCHSpace.fromBase,
  	toBase: LCHSpace.toBase,

  	formats: {
  		// https://drafts.csswg.org/css-color-hdr/#JzCzhz
  		jzczhz: {
  			coords: ["<percentage> | <number>", "<number> | <percentage>", "<number> | <angle>"],
  		},
  	},
  });

  /**
   * More accurate color-difference formulae
   * than the simple 1976 Euclidean distance in Lab
   *
   * Uses JzCzHz, which has improved perceptual uniformity
   * and thus a simple Euclidean root-sum of ΔL² ΔC² ΔH²
   * gives good results.
   * @param {import("../types.js").ColorTypes} color
   * @param {import("../types.js").ColorTypes} sample
   * @returns {number}
   */
  function deltaEJz (color, sample) {
  	[color, sample] = getColor([color, sample]);

  	// Given this color as the reference
  	// and a sample,
  	// calculate deltaE in JzCzHz.
  	let [Jz1, Cz1, Hz1] = jzczhz.from(color);
  	let [Jz2, Cz2, Hz2] = jzczhz.from(sample);

  	// Lightness and Chroma differences
  	// sign does not matter as they are squared.
  	let ΔJ = Jz1 - Jz2;
  	let ΔC = Cz1 - Cz2;

  	// length of chord for ΔH
  	if (isNone(Hz1) && isNone(Hz2)) {
  		// both undefined hues
  		Hz1 = 0;
  		Hz2 = 0;
  	}
  	else if (isNone(Hz1)) {
  		// one undefined, set to the defined hue
  		Hz1 = Hz2;
  	}
  	else if (isNone(Hz2)) {
  		Hz2 = Hz1;
  	}

  	let Δh = Hz1 - Hz2;
  	let ΔH = 2 * Math.sqrt(Cz1 * Cz2) * Math.sin((Δh / 2) * (Math.PI / 180));

  	return Math.sqrt(ΔJ ** 2 + ΔC ** 2 + ΔH ** 2);
  }

  /** @import { Matrix3x3, Vector3 } from "../types.js" */

  const c1 = 3424 / 4096;
  const c2 = 2413 / 128;
  const c3 = 2392 / 128;
  const m1$1 = 2610 / 16384;
  const m2 = 2523 / 32;
  const im1 = 16384 / 2610;
  const im2 = 32 / 2523;

  // The matrix below includes the 4% crosstalk components
  // and is from the Dolby "What is ICtCp" paper"
  /** @type {Matrix3x3} */
  // prettier-ignore
  const XYZtoLMS_M = [
  	[  0.3592832590121217,  0.6976051147779502, -0.035891593232029 ],
  	[ -0.1920808463704993,  1.1004767970374321,  0.0753748658519118 ],
  	[  0.0070797844607479,  0.0748396662186362,  0.8433265453898765 ],
  ];
  // linear-light Rec.2020 to LMS, again with crosstalk
  // rational terms from Jan Fröhlich,
  // Encoding High Dynamic Range andWide Color Gamut Imagery, p.97
  // and ITU-R BT.2124-0 p.2
  /*
  const Rec2020toLMS_M = [
  	[ 1688 / 4096,  2146 / 4096,   262 / 4096 ],
  	[  683 / 4096,  2951 / 4096,   462 / 4096 ],
  	[   99 / 4096,   309 / 4096,  3688 / 4096 ]
  ];
  */
  // this includes the Ebner LMS coefficients,
  // the rotation, and the scaling to [-0.5,0.5] range
  // rational terms from Fröhlich p.97
  // and ITU-R BT.2124-0 pp.2-3
  /** @type {Matrix3x3} */
  // prettier-ignore
  const LMStoIPT_M = [
  	[  2048 / 4096,   2048 / 4096,       0      ],
  	[  6610 / 4096, -13613 / 4096,  7003 / 4096 ],
  	[ 17933 / 4096, -17390 / 4096,  -543 / 4096 ],
  ];

  // inverted matrices, calculated from the above
  /** @type {Matrix3x3} */
  // prettier-ignore
  const IPTtoLMS_M = [
  	[ 0.9999999999999998,  0.0086090370379328,  0.1110296250030260 ],
  	[ 0.9999999999999998, -0.0086090370379328, -0.1110296250030259 ],
  	[ 0.9999999999999998,  0.5600313357106791, -0.3206271749873188 ],
  ];
  /*
  // prettier-ignore
  const LMStoRec2020_M = [
  	[ 3.4375568932814012112,   -2.5072112125095058195,   0.069654319228104608382],
  	[-0.79142868665644156125,   1.9838372198740089874,  -0.19240853321756742626 ],
  	[-0.025646662911506476363, -0.099240248643945566751, 1.1248869115554520431  ]
  ];
  */
  /** @type {Matrix3x3} */
  // prettier-ignore
  const LMStoXYZ_M = [
  	[  2.0701522183894223, -1.3263473389671563,  0.2066510476294053 ],
  	[  0.3647385209748072,  0.6805660249472273, -0.0453045459220347 ],
  	[ -0.0497472075358123, -0.0492609666966131,  1.1880659249923042 ],
  ];

  // Only the PQ form of ICtCp is implemented here. There is also an HLG form.
  // from Dolby, "WHAT IS ICTCP?"
  // https://professional.dolby.com/siteassets/pdfs/ictcp_dolbywhitepaper_v071.pdf
  // and
  // Dolby, "Perceptual Color Volume
  // Measuring the Distinguishable Colors of HDR and WCG Displays"
  // https://professional.dolby.com/siteassets/pdfs/dolby-vision-measuring-perceptual-color-volume-v7.1.pdf
  var ictcp = new ColorSpace({
  	id: "ictcp",
  	name: "ICTCP",
  	// From BT.2100-2 page 7:
  	// During production, signal values are expected to exceed the
  	// range E′ = [0.0 : 1.0]. This provides processing headroom and avoids
  	// signal degradation during cascaded processing. Such values of E′,
  	// below 0.0 or exceeding 1.0, should not be clipped during production
  	// and exchange.
  	// Values below 0.0 should not be clipped in reference displays (even
  	// though they represent “negative” light) to allow the black level of
  	// the signal (LB) to be properly set using test signals known as “PLUGE”
  	coords: {
  		i: {
  			refRange: [0, 1], // Constant luminance,
  			name: "I",
  		},
  		ct: {
  			refRange: [-0.5, 0.5], // Full BT.2020 gamut in range [-0.5, 0.5]
  			name: "CT",
  		},
  		cp: {
  			refRange: [-0.5, 0.5],
  			name: "CP",
  		},
  	},

  	base: XYZ_Abs_D65,
  	fromBase (XYZ) {
  		// move to LMS cone domain
  		let LMS = multiply_v3_m3x3(XYZ, XYZtoLMS_M);

  		return LMStoICtCp(LMS);
  	},
  	toBase (ICtCp) {
  		let LMS = ICtCptoLMS(ICtCp);

  		return multiply_v3_m3x3(LMS, LMStoXYZ_M);
  	},

  	formats: {
  		ictcp: {
  			coords: [
  				"<percentage> | <number>",
  				"<number> | <percentage>",
  				"<number> | <percentage>",
  			],
  		},
  	},
  });

  /**
   *
   * @param {Vector3} LMS
   * @returns {Vector3}
   */
  function LMStoICtCp (LMS) {
  	// apply the PQ EOTF
  	// we can't ever be dividing by zero because of the "1 +" in the denominator
  	let PQLMS = /** @type {Vector3} */ (
  		LMS.map(function (val) {
  			let num = c1 + c2 * (val / 10000) ** m1$1;
  			let denom = 1 + c3 * (val / 10000) ** m1$1;

  			return (num / denom) ** m2;
  		})
  	);

  	// LMS to IPT, with rotation for Y'C'bC'r compatibility
  	return multiply_v3_m3x3(PQLMS, LMStoIPT_M);
  }

  /**
   *
   * @param {Vector3} ICtCp
   * @returns {Vector3}
   */
  function ICtCptoLMS (ICtCp) {
  	let PQLMS = multiply_v3_m3x3(ICtCp, IPTtoLMS_M);

  	// From BT.2124-0 Annex 2 Conversion 3
  	let LMS = /** @type {Vector3} */ (
  		PQLMS.map(function (val) {
  			let num = Math.max(val ** im2 - c1, 0);
  			let denom = c2 - c3 * val ** im2;
  			return 10000 * (num / denom) ** im1;
  		})
  	);

  	return LMS;
  }

  /**
   * Delta E in ICtCp space,
   * which the ITU calls Delta E ITP, which is shorter.
   * Formulae from ITU Rec. ITU-R BT.2124-0
   * @param {import("../types.js").ColorTypes} color
   * @param {import("../types.js").ColorTypes} sample
   * @returns {number}
   */
  function deltaEITP (color, sample) {
  	[color, sample] = getColor([color, sample]);

  	// Given this color as the reference
  	// and a sample,
  	// calculate deltaE in ICtCp
  	// which is simply the Euclidean distance

  	let [I1, T1, P1] = ictcp.from(color);
  	let [I2, T2, P2] = ictcp.from(sample);

  	// the 0.25 factor is to undo the encoding scaling in Ct
  	// the 720 is so that 1 deltaE = 1 JND
  	// per  ITU-R BT.2124-0 p.3

  	return 720 * Math.sqrt((I1 - I2) ** 2 + 0.25 * (T1 - T2) ** 2 + (P1 - P2) ** 2);
  }

  /**
   * More accurate color-difference formulae
   * than the simple 1976 Euclidean distance in CIE Lab
   * The Oklab a and b axes are scaled relative to the L axis, for better uniformity
   * Björn Ottosson said:
   * "I've recently done some tests with color distance datasets as implemented
   * in Colorio and on both the Combvd dataset and the OSA-UCS dataset a
   * scale factor of slightly more than 2 for a and b would give the best results
   * (2.016 works best for Combvd and 2.045 for the OSA-UCS dataset)."
   * @see {@link <https://github.com/w3c/csswg-drafts/issues/6642#issuecomment-945714988>}
   * @param {import("../types.js").ColorTypes} color
   * @param {import("../types.js").ColorTypes} sample
   * @returns {number}
   */
  function deltaEOK2 (color, sample) {
  	[color, sample] = getColor([color, sample]);

  	// Given this color as the reference
  	// and a sample,
  	// calculate deltaEOK2, term by term as root sum of squares
  	let abscale = 2;
  	let [L1, a1, b1] = OKLab.from(color);
  	let [L2, a2, b2] = OKLab.from(sample);
  	let ΔL = L1 - L2;
  	let Δa = abscale * (a1 - a2);
  	let Δb = abscale * (b1 - b2);
  	return Math.sqrt(ΔL ** 2 + Δa ** 2 + Δb ** 2);
  }

  /** @import { Coords, Matrix3x3, Vector3 } from "../types.js" */

  // Type re-exports
  /** @typedef {import("../types.js").Cam16Object} Cam16Object */
  /** @typedef {import("../types.js").Cam16Input} Cam16Input */
  /** @typedef {import("../types.js").Cam16Environment} Cam16Environment */

  const white$1 = WHITES.D65;
  const adaptedCoef = 0.42;
  const adaptedCoefInv = 1 / adaptedCoef;
  const tau = 2 * Math.PI;

  /** @type {Matrix3x3} */
  // prettier-ignore
  const cat16 = [
  	[  0.401288,  0.650173, -0.051461 ],
  	[ -0.250268,  1.204414,  0.045854 ],
  	[ -2079e-6,  0.048952,  0.953127 ],
  ];

  /** @type {Matrix3x3} */
  const cat16Inv = [
  	[1.8620678550872327, -1.0112546305316843, 0.14918677544445175],
  	[0.38752654323613717, 0.6214474419314753, -0.008973985167612518],
  	[-0.015841498849333856, -0.03412293802851557, 1.0499644368778496],
  ];

  /** @type {Matrix3x3} */
  const m1 = [
  	[460.0, 451.0, 288.0],
  	[460.0, -891, -261],
  	[460.0, -220, -6300],
  ];

  const surroundMap = {
  	dark: [0.8, 0.525, 0.8],
  	dim: [0.9, 0.59, 0.9],
  	average: [1, 0.69, 1],
  };

  const hueQuadMap = {
  	// Red, Yellow, Green, Blue, Red
  	h: [20.14, 90.0, 164.25, 237.53, 380.14],
  	e: [0.8, 0.7, 1.0, 1.2, 0.8],
  	H: [0.0, 100.0, 200.0, 300.0, 400.0],
  };

  const rad2deg = 180 / Math.PI;
  const deg2rad$1 = Math.PI / 180;

  /**
   * @param {Coords} coords
   * @param {number} fl
   * @returns {[number, number, number]}
   */
  function adapt (coords, fl) {
  	const temp = /** @type {[number, number, number]} */ (
  		coords.map(c => {
  			const x = spow(fl * Math.abs(c) * 0.01, adaptedCoef);
  			return (400 * copySign(x, c)) / (x + 27.13);
  		})
  	);
  	return temp;
  }

  /**
   * @param {Coords} adapted
   * @param {number} fl
   * @returns {[number, number, number]}
   */
  function unadapt (adapted, fl) {
  	const constant = (100 / fl) * 27.13 ** adaptedCoefInv;
  	return /** @type {[number, number, number]} */ (
  		adapted.map(c => {
  			const cabs = Math.abs(c);
  			return copySign(constant * spow(cabs / (400 - cabs), adaptedCoefInv), c);
  		})
  	);
  }

  /**
   * @param {number} h
   */
  function hueQuadrature (h) {
  	let hp = constrain$1(h);
  	if (hp <= hueQuadMap.h[0]) {
  		hp += 360;
  	}

  	const i = bisectLeft(hueQuadMap.h, hp) - 1;
  	const [hi, hii] = hueQuadMap.h.slice(i, i + 2);
  	const [ei, eii] = hueQuadMap.e.slice(i, i + 2);
  	const Hi = hueQuadMap.H[i];

  	const t = (hp - hi) / ei;
  	return Hi + (100 * t) / (t + (hii - hp) / eii);
  }

  /**
   * @param {number} H
   */
  function invHueQuadrature (H) {
  	let Hp = ((H % 400) + 400) % 400;
  	const i = Math.floor(0.01 * Hp);
  	Hp = Hp % 100;
  	const [hi, hii] = hueQuadMap.h.slice(i, i + 2);
  	const [ei, eii] = hueQuadMap.e.slice(i, i + 2);

  	return constrain$1((Hp * (eii * hi - ei * hii) - 100 * hi * eii) / (Hp * (eii - ei) - 100 * eii));
  }

  /**
   * @param {[number, number, number]} refWhite
   * @param {number} adaptingLuminance
   * @param {number} backgroundLuminance
   * @param {keyof typeof surroundMap} surround
   * @param {boolean} discounting
   * @returns {Cam16Environment}
   */
  function environment (
  	refWhite,
  	adaptingLuminance,
  	backgroundLuminance,
  	surround,
  	discounting,
  ) {
  	const env = {};

  	env.discounting = discounting;
  	env.refWhite = refWhite;
  	env.surround = surround;
  	const xyzW = /** @type {Vector3} */ (
  		refWhite.map(c => {
  			return c * 100;
  		})
  	);

  	// The average luminance of the environment in `cd/m^2cd/m` (a.k.a. nits)
  	env.la = adaptingLuminance;
  	// The relative luminance of the nearby background
  	env.yb = backgroundLuminance;
  	// Absolute luminance of the reference white.
  	const yw = xyzW[1];

  	// Cone response for reference white
  	const rgbW = multiply_v3_m3x3(xyzW, cat16);

  	// Surround: dark, dim, and average
  	let values = surroundMap[env.surround];
  	const f = values[0];
  	env.c = values[1];
  	env.nc = values[2];

  	const k = 1 / (5 * env.la + 1);
  	const k4 = k ** 4;

  	// Factor of luminance level adaptation
  	env.fl = k4 * env.la + 0.1 * (1 - k4) * (1 - k4) * Math.cbrt(5 * env.la);
  	env.flRoot = env.fl ** 0.25;

  	env.n = env.yb / yw;
  	env.z = 1.48 + Math.sqrt(env.n);
  	env.nbb = 0.725 * env.n ** -0.2;
  	env.ncb = env.nbb;

  	// Degree of adaptation calculating if not discounting
  	// illuminant (assumed eye is fully adapted)
  	const d = Math.max(Math.min(f * (1 - (1 / 3.6) * Math.exp((-env.la - 42) / 92)), 1), 0);
  	env.dRgb = /** @type {[number, number, number]} */ (
  		rgbW.map(c => {
  			return interpolate(1, yw / c, d);
  		})
  	);
  	env.dRgbInv = /** @type {[number, number, number]} */ (
  		env.dRgb.map(c => {
  			return 1 / c;
  		})
  	);

  	// Achromatic response
  	const rgbCW = /** @type {[number, number, number]} */ (
  		rgbW.map((c, i) => {
  			return c * env.dRgb[i];
  		})
  	);
  	const rgbAW = adapt(rgbCW, env.fl);
  	env.aW = env.nbb * (2 * rgbAW[0] + rgbAW[1] + 0.05 * rgbAW[2]);

  	// console.log(env);

  	return env;
  }

  // Pre-calculate everything we can with the viewing conditions
  const viewingConditions$1 = environment(white$1, (64 / Math.PI) * 0.2, 20, "average", false);

  /**
   * @param {Cam16Input} cam16
   * @param {Cam16Environment} env
   * @returns {[number, number, number]}
   */
  function fromCam16 (cam16, env) {
  	// These check ensure one, and only one attribute for a
  	// given category is provided.
  	// @ts-expect-error The '^` operator is not allowed for boolean types
  	if (!((cam16.J !== undefined) ^ (cam16.Q !== undefined))) {
  		throw new Error("Conversion requires one and only one: 'J' or 'Q'");
  	}

  	// @ts-expect-error - The '^` operator is not allowed for boolean types
  	if (!((cam16.C !== undefined) ^ (cam16.M !== undefined) ^ (cam16.s !== undefined))) {
  		throw new Error("Conversion requires one and only one: 'C', 'M' or 's'");
  	}

  	// Hue is absolutely required
  	// @ts-expect-error - The '^` operator is not allowed for boolean types
  	if (!((cam16.h !== undefined) ^ (cam16.H !== undefined))) {
  		throw new Error("Conversion requires one and only one: 'h' or 'H'");
  	}

  	// Black
  	if (cam16.J === 0.0 || cam16.Q === 0.0) {
  		return [0.0, 0.0, 0.0];
  	}

  	// Break hue into Cartesian components
  	let hRad = 0.0;
  	if (cam16.h !== undefined) {
  		hRad = constrain$1(cam16.h) * deg2rad$1;
  	}
  	else {
  		hRad = invHueQuadrature(cam16.H) * deg2rad$1;
  	}

  	const cosh = Math.cos(hRad);
  	const sinh = Math.sin(hRad);

  	// Calculate `Jroot` from one of the lightness derived coordinates.
  	let Jroot = 0.0;
  	if (cam16.J !== undefined) {
  		Jroot = spow(cam16.J, 1 / 2) * 0.1;
  	}
  	else if (cam16.Q !== undefined) {
  		Jroot = (0.25 * env.c * cam16.Q) / ((env.aW + 4) * env.flRoot);
  	}

  	// Calculate the `t` value from one of the chroma derived coordinates
  	let alpha = 0.0;
  	if (cam16.C !== undefined) {
  		alpha = cam16.C / Jroot;
  	}
  	else if (cam16.M !== undefined) {
  		alpha = cam16.M / env.flRoot / Jroot;
  	}
  	else if (cam16.s !== undefined) {
  		alpha = (0.0004 * cam16.s ** 2 * (env.aW + 4)) / env.c;
  	}
  	const t = spow(alpha * Math.pow(1.64 - Math.pow(0.29, env.n), -0.73), 10 / 9);

  	// Eccentricity
  	const et = 0.25 * (Math.cos(hRad + 2) + 3.8);

  	// Achromatic response
  	const A = env.aW * spow(Jroot, 2 / env.c / env.z);

  	// Calculate red-green and yellow-blue components
  	const p1 = (5e4 / 13) * env.nc * env.ncb * et;
  	const p2 = A / env.nbb;
  	const r = 23 * (p2 + 0.305) * zdiv(t, 23 * p1 + t * (11 * cosh + 108 * sinh));
  	const a = r * cosh;
  	const b = r * sinh;

  	// Calculate back from cone response to XYZ
  	const rgb_c = unadapt(
  		/** @type {Vector3} */
  		(
  			multiply_v3_m3x3([p2, a, b], m1).map(c => {
  				return (c * 1) / 1403;
  			})
  		),
  		env.fl,
  	);
  	return /** @type {Vector3} */ (
  		multiply_v3_m3x3(
  			/** @type {Vector3} */ (
  				rgb_c.map((c, i) => {
  					return c * env.dRgbInv[i];
  				})
  			),
  			cat16Inv,
  		).map(c => {
  			return c / 100;
  		})
  	);
  }

  /**
   * @param {[number, number, number]} xyzd65
   * @param {Cam16Environment} env
   * @returns {Cam16Object}
   */
  function toCam16 (xyzd65, env) {
  	// Cone response
  	const xyz100 = /** @type {Vector3} */ (
  		xyzd65.map(c => {
  			return c * 100;
  		})
  	);
  	const rgbA = adapt(
  		/** @type {[number, number, number]} */
  		(
  			multiply_v3_m3x3(xyz100, cat16).map((c, i) => {
  				return c * env.dRgb[i];
  			})
  		),
  		env.fl,
  	);

  	// Calculate hue from red-green and yellow-blue components
  	const a = rgbA[0] + (-12 * rgbA[1] + rgbA[2]) / 11;
  	const b = (rgbA[0] + rgbA[1] - 2 * rgbA[2]) / 9;
  	const hRad = ((Math.atan2(b, a) % tau) + tau) % tau;

  	// Eccentricity
  	const et = 0.25 * (Math.cos(hRad + 2) + 3.8);

  	const t =
  		(5e4 / 13) *
  		env.nc *
  		env.ncb *
  		zdiv(et * Math.sqrt(a ** 2 + b ** 2), rgbA[0] + rgbA[1] + 1.05 * rgbA[2] + 0.305);
  	const alpha = spow(t, 0.9) * Math.pow(1.64 - Math.pow(0.29, env.n), 0.73);

  	// Achromatic response
  	const A = env.nbb * (2 * rgbA[0] + rgbA[1] + 0.05 * rgbA[2]);

  	const Jroot = spow(A / env.aW, 0.5 * env.c * env.z);

  	// Lightness
  	const J = 100 * spow(Jroot, 2);

  	// Brightness
  	const Q = (4 / env.c) * Jroot * (env.aW + 4) * env.flRoot;

  	// Chroma
  	const C = alpha * Jroot;

  	// Colorfulness
  	const M = C * env.flRoot;

  	// Hue
  	const h = constrain$1(hRad * rad2deg);

  	// Hue quadrature
  	const H = hueQuadrature(h);

  	// Saturation
  	const s = 50 * spow((env.c * alpha) / (env.aW + 4), 1 / 2);

  	// console.log({J: J, C: C, h: h, s: s, Q: Q, M: M, H: H});

  	return { J: J, C: C, h: h, s: s, Q: Q, M: M, H: H };
  }

  // Provided as a way to directly evaluate the CAM16 model
  // https://observablehq.com/@jrus/cam16: reference implementation
  // https://arxiv.org/pdf/1802.06067.pdf: Nico Schlömer
  // https://onlinelibrary.wiley.com/doi/pdf/10.1002/col.22324: hue quadrature
  // https://www.researchgate.net/publication/318152296_Comprehensive_color_solutions_CAM16_CAT16_and_CAM16-UCS
  // Results compared against: https://github.com/colour-science/colour
  new ColorSpace({
  	id: "cam16-jmh",
  	cssId: "--cam16-jmh",
  	name: "CAM16-JMh",
  	coords: {
  		j: {
  			refRange: [0, 100],
  			name: "J",
  		},
  		m: {
  			refRange: [0, 105.0],
  			name: "Colorfulness",
  		},
  		h: {
  			refRange: [0, 360],
  			type: "angle",
  			name: "Hue",
  		},
  	},

  	base: xyz_d65,

  	fromBase (xyz) {
  		// If another derivation is created, ε could vary, so we can't hardcode
  		if (this.ε === undefined) {
  			this.ε = Object.values(this.coords)[1].refRange[1] / 100000;
  		}
  		const cam16 = toCam16(xyz, viewingConditions$1);
  		const isAchromatic = Math.abs(cam16.M) < this.ε;
  		return [cam16.J, isAchromatic ? 0 : cam16.M, isAchromatic ? null : cam16.h];
  	},
  	toBase (cam16) {
  		return fromCam16({ J: cam16[0], M: cam16[1], h: cam16[2] }, viewingConditions$1);
  	},
  });

  const white = WHITES.D65;
  const ε = 216 / 24389; // 6^3/29^3 == (24/116)^3
  const κ = 24389 / 27; // 29^3/3^3

  function toLstar (y) {
  	// Convert XYZ Y to L*

  	const fy = y > ε ? Math.cbrt(y) : (κ * y + 16) / 116;
  	return 116.0 * fy - 16.0;
  }

  function fromLstar (lstar) {
  	// Convert L* back to XYZ Y

  	return lstar > 8 ? Math.pow((lstar + 16) / 116, 3) : lstar / κ;
  }

  function fromHct (coords, env) {
  	// Use Newton's method to try and converge as quick as possible or
  	// converge as close as we can. While the requested precision is achieved
  	// most of the time, it may not always be achievable. Especially past the
  	// visible spectrum, the algorithm will likely struggle to get the same
  	// precision. If, for whatever reason, we cannot achieve the accuracy we
  	// seek in the allotted iterations, just return the closest we were able to
  	// get.

  	let [h, c, t] = coords;
  	let xyz = [];
  	let j = 0;

  	// Shortcut out for black
  	if (t === 0) {
  		return [0.0, 0.0, 0.0];
  	}

  	// Calculate the Y we need to target
  	let y = fromLstar(t);

  	// A better initial guess yields better results. Polynomials come from
  	// curve fitting the T vs J response.
  	if (t > 0) {
  		j = 0.00379058511492914 * t ** 2 + 0.608983189401032 * t + 0.9155088574762233;
  	}
  	else {
  		j = 9.514440756550361e-6 * t ** 2 + 0.08693057439788597 * t - 21.928975842194614;
  	}

  	// Threshold of how close is close enough, and max number of attempts.
  	// More precision and more attempts means more time spent iterating. Higher
  	// required precision gives more accuracy but also increases the chance of
  	// not hitting the goal. 2e-12 allows us to convert round trip with
  	// reasonable accuracy of six decimal places or more.
  	const threshold = 2e-12;
  	const max_attempts = 15;

  	let attempt = 0;
  	let last = Infinity;

  	// Try to find a J such that the returned y matches the returned y of the L*
  	while (attempt <= max_attempts) {
  		xyz = fromCam16({ J: j, C: c, h: h }, env);

  		// If we are within range, return XYZ
  		// If we are closer than last time, save the values
  		const delta = Math.abs(xyz[1] - y);
  		if (delta < last) {
  			if (delta <= threshold) {
  				return xyz;
  			}
  			last = delta;
  		}

  		// f(j_root) = (j ** (1 / 2)) * 0.1
  		// f(j) = ((f(j_root) * 100) ** 2) / j - 1 = 0
  		// f(j_root) = Y = y / 100
  		// f(j) = (y ** 2) / j - 1
  		// f'(j) = (2 * y) / j
  		j = j - ((xyz[1] - y) * j) / (2 * xyz[1]);

  		attempt += 1;
  	}

  	// We could not acquire the precision we desired,
  	// return our closest attempt.
  	return fromCam16({ J: j, C: c, h: h }, env);
  }

  function toHct (xyz, env) {
  	// Calculate HCT by taking the L* of CIE LCh D65 and CAM16 chroma and hue.

  	const t = toLstar(xyz[1]);
  	if (t === 0.0) {
  		return [0.0, 0.0, 0.0];
  	}
  	const cam16 = toCam16(xyz, viewingConditions);
  	return [constrain$1(cam16.h), cam16.C, t];
  }

  // Pre-calculate everything we can with the viewing conditions
  const viewingConditions = environment(
  	white,
  	(200 / Math.PI) * fromLstar(50.0),
  	fromLstar(50.0) * 100,
  	"average",
  	false,
  );

  // https://material.io/blog/science-of-color-design
  // This is not a port of the material-color-utilities,
  // but instead implements the full color space as described,
  // combining CAM16 JCh and Lab D65. This does not clamp conversion
  // to HCT to specific chroma bands and provides support for wider
  // gamuts than Google currently supports and does so at a greater
  // precision (> 8 bits back to sRGB).
  // This implementation comes from https://github.com/facelessuser/coloraide
  // which is licensed under MIT.
  var hct = new ColorSpace({
  	id: "hct",
  	name: "HCT",
  	coords: {
  		h: {
  			refRange: [0, 360],
  			type: "angle",
  			name: "Hue",
  		},
  		c: {
  			refRange: [0, 145],
  			name: "Colorfulness",
  		},
  		t: {
  			refRange: [0, 100],
  			name: "Tone",
  		},
  	},

  	base: xyz_d65,

  	fromBase (xyz) {
  		if (this.ε === undefined) {
  			this.ε = Object.values(this.coords)[1].refRange[1] / 100000;
  		}
  		let hct = toHct(xyz);
  		if (hct[1] < this.ε) {
  			hct[1] = 0.0;
  			hct[0] = null;
  		}
  		return hct;
  	},
  	toBase (hct) {
  		return fromHct(hct, viewingConditions);
  	},
  	formats: {
  		color: {
  			id: "--hct",
  			coords: ["<number> | <angle>", "<percentage> | <number>", "<percentage> | <number>"],
  		},
  	},
  });

  const deg2rad = Math.PI / 180;
  const ucsCoeff = [1.0, 0.007, 0.0228];

  /**
   * Convert HCT chroma and hue (CAM16 JMh colorfulness and hue) using UCS logic for a and b.
   * @param {Coords} coords - HCT coordinates.
   * @return {number[]}
   */
  function convertUcsAb (coords) {
  	// We want the distance between the actual color.
  	// If chroma is negative, it will throw off our calculations.
  	// Normally, converting back to the base and forward will correct it.
  	// If we have a negative chroma after this, then we have a color that
  	// cannot resolve to positive chroma.
  	if (coords[1] < 0) {
  		coords = hct.fromBase(hct.toBase(coords));
  	}

  	// Only in extreme cases (usually outside the visible spectrum)
  	// can the input value for log become negative.
  	// Avoid domain error by forcing a zero result via "max" if necessary.
  	const M =
  		Math.log(Math.max(1 + ucsCoeff[2] * coords[1] * viewingConditions.flRoot, 1.0)) /
  		ucsCoeff[2];
  	const hrad = coords[0] * deg2rad;
  	const a = M * Math.cos(hrad);
  	const b = M * Math.sin(hrad);

  	return [coords[2], a, b];
  }

  /**
   * Color distance using HCT.
   * @param {import("../types.js").ColorTypes} color
   * @param {import("../types.js").ColorTypes} sample
   * @returns {number}
   */
  function deltaEHCT (color, sample) {
  	[color, sample] = getColor([color, sample]);

  	let [t1, a1, b1] = convertUcsAb(hct.from(color));
  	let [t2, a2, b2] = convertUcsAb(hct.from(sample));

  	// Use simple euclidean distance with a and b using UCS conversion
  	// and LCh lightness (HCT tone).
  	return Math.sqrt((t1 - t2) ** 2 + (a1 - a2) ** 2 + (b1 - b2) ** 2);
  }

  /**
   * @packageDocumentation
   * This module defines all the builtin deltaE methods.
   */

  var deltaEMethods = {
  	deltaE76,
  	deltaECMC,
  	deltaE2000,
  	deltaEJz,
  	deltaEITP,
  	deltaEOK,
  	deltaEOK2,
  	deltaEHCT,
  };

  /** @typedef {keyof typeof import("./index.js").default extends `deltaE${infer Method}` ? Method : string} Methods */

  /** @import { ColorTypes, PlainColorObject } from "./types.js" */

  // Type re-exports
  /** @typedef {import("./types.js").ToGamutOptions} ToGamutOptions */

  /**
   * Calculate the epsilon to 2 degrees smaller than the specified JND.
   * @param {number} jnd The target "just noticeable difference".
   * @returns {number}
   */
  function calcEpsilon (jnd) {
  	// Calculate the epsilon to 2 degrees smaller than the specified JND.

  	const order = !jnd ? 0 : Math.floor(Math.log10(Math.abs(jnd)));
  	// Limit to an arbitrary value to ensure value is never too small and causes infinite loops.
  	return Math.max(parseFloat(`1e${order - 2}`), 1e-6);
  }

  const GMAPPRESET = {
  	hct: {
  		method: "hct.c",
  		jnd: 2,
  		deltaEMethod: "hct",
  		blackWhiteClamp: {},
  	},
  	"hct-tonal": {
  		method: "hct.c",
  		jnd: 0,
  		deltaEMethod: "hct",
  		blackWhiteClamp: { channel: "hct.t", min: 0, max: 100 },
  	},
  };

  /**
   * Force coordinates to be in gamut of a certain color space.
   * Mutates the color it is passed.
   * @overload
   * @param {ColorTypes} color
   * @param {ToGamutOptions} [options]
   * @returns {PlainColorObject}
   */
  /**
   * @overload
   * @param {ColorTypes} color
   * @param {string} [space]
   * @returns {PlainColorObject}
   */
  /**
   * @param {ColorTypes} color
   * @param {string & Partial<ToGamutOptions> | ToGamutOptions} [space]
   * @returns {PlainColorObject}
   */
  function toGamut (
  	color,
  	{
  		method = defaults$1.gamut_mapping,
  		space = undefined,
  		deltaEMethod = "",
  		jnd = 2,
  		blackWhiteClamp = undefined,
  	} = {},
  ) {
  	color = getColor(color);

  	if (isString(arguments[1])) {
  		space = arguments[1];
  	}
  	else if (!space) {
  		space = color.space;
  	}

  	space = ColorSpace.get(space);

  	// 3 spaces:
  	// color.space: current color space
  	// space: space whose gamut we are mapping to
  	// mapSpace: space with the coord we're reducing

  	if (inGamut(color, space, { epsilon: 0 })) {
  		return /** @type {PlainColorObject} */ (color);
  	}

  	let spaceColor;
  	if (method === "css") {
  		spaceColor = toGamutCSS(color, { space });
  	}
  	else {
  		if (method !== "clip" && !inGamut(color, space)) {
  			if (Object.prototype.hasOwnProperty.call(GMAPPRESET, method)) {
  				({ method, jnd, deltaEMethod, blackWhiteClamp } = GMAPPRESET[method]);
  			}

  			// Get the correct delta E method
  			let de = deltaE2000;
  			if (deltaEMethod !== "") {
  				for (let m in deltaEMethods) {
  					if ("deltae" + deltaEMethod.toLowerCase() === m.toLowerCase()) {
  						de = deltaEMethods[m];
  						break;
  					}
  				}
  			}

  			if (jnd === 0) {
  				jnd = 1e-16;
  			}

  			let clipped = toGamut(to(color, space), { method: "clip", space });
  			if (de(color, clipped) > jnd) {
  				// Clamp to SDR white and black if required
  				if (blackWhiteClamp && Object.keys(blackWhiteClamp).length === 3) {
  					let channelMeta = ColorSpace.resolveCoord(blackWhiteClamp.channel);
  					let channel = get$1(to(color, channelMeta.space), channelMeta.id);
  					if (isNone(channel)) {
  						channel = 0;
  					}
  					if (channel >= blackWhiteClamp.max) {
  						return to({ space: "xyz-d65", coords: WHITES["D65"] }, color.space);
  					}
  					else if (channel <= blackWhiteClamp.min) {
  						return to({ space: "xyz-d65", coords: [0, 0, 0] }, color.space);
  					}
  				}

  				// Reduce a coordinate of a certain color space until the color is in gamut
  				let coordMeta = ColorSpace.resolveCoord(method);
  				let mapSpace = coordMeta.space;
  				let coordId = coordMeta.id;

  				let mappedColor = to(color, mapSpace);
  				// If we were already in the mapped color space, we need to resolve undefined channels
  				mappedColor.coords.forEach((c, i) => {
  					if (isNone(c)) {
  						mappedColor.coords[i] = 0;
  					}
  				});
  				let bounds = coordMeta.range || coordMeta.refRange;
  				let min = bounds[0];
  				let ε = calcEpsilon(jnd);
  				let low = min;
  				let high = get$1(mappedColor, coordId);

  				while (high - low > ε) {
  					let clipped = clone$1(mappedColor);
  					clipped = toGamut(clipped, { space, method: "clip" });
  					let deltaE = de(mappedColor, clipped);

  					if (deltaE - jnd < ε) {
  						low = get$1(mappedColor, coordId);
  					}
  					else {
  						high = get$1(mappedColor, coordId);
  					}

  					set(mappedColor, coordId, (low + high) / 2);
  				}

  				spaceColor = to(mappedColor, space);
  			}
  			else {
  				spaceColor = clipped;
  			}
  		}
  		else {
  			spaceColor = to(color, space);
  		}

  		if (
  			method === "clip" || // Dumb coord clipping
  			// finish off smarter gamut mapping with clip to get rid of ε, see #17
  			!inGamut(spaceColor, space, { epsilon: 0 })
  		) {
  			let bounds = Object.values(space.coords).map(c => c.range || []);

  			spaceColor.coords = /** @type {[number, number, number]} */ (
  				spaceColor.coords.map((c, i) => {
  					let [min, max] = bounds[i];

  					if (min !== undefined) {
  						c = Math.max(min, c);
  					}

  					if (max !== undefined) {
  						c = Math.min(c, max);
  					}

  					return c;
  				})
  			);
  		}
  	}

  	if (space !== color.space) {
  		spaceColor = to(spaceColor, color.space);
  	}

  	color.coords = spaceColor.coords;
  	return /** @type {PlainColorObject} */ (color);
  }

  /** @type {"color"} */
  toGamut.returns = "color";

  /**
   * The reference colors to be used if lightness is out of the range 0-1 in the
   * `Oklch` space. These are created in the `Oklab` space, as it is used by the
   * DeltaEOK calculation, so it is guaranteed to be imported.
   * @satisfies {Record<string, ColorTypes>}
   */
  const COLORS = {
  	WHITE: { space: OKLab, coords: [1, 0, 0], alpha: 1 },
  	BLACK: { space: OKLab, coords: [0, 0, 0], alpha: 1 },
  };

  /**
   * Given a color `origin`, returns a new color that is in gamut using
   * the CSS Gamut Mapping Algorithm. If `space` is specified, it will be in gamut
   * in `space`, and returned in `space`. Otherwise, it will be in gamut and
   * returned in the color space of `origin`.
   * @param {ColorTypes} origin
   * @param {{ space?: string | ColorSpace | undefined }} param1
   * @returns {PlainColorObject}
   */
  function toGamutCSS (origin, { space } = {}) {
  	const JND = 0.02;
  	const ε = 0.0001;

  	origin = getColor(origin);

  	if (!space) {
  		space = origin.space;
  	}

  	space = ColorSpace.get(space);
  	const oklchSpace = ColorSpace.get("oklch");

  	if (space.isUnbounded) {
  		return to(origin, space);
  	}

  	const origin_OKLCH = to(origin, oklchSpace);
  	let L = origin_OKLCH.coords[0];

  	// return media white or black, if lightness is out of range
  	if (L >= 1) {
  		const white = to(COLORS.WHITE, space);
  		white.alpha = origin.alpha;
  		return to(white, space);
  	}
  	if (L <= 0) {
  		const black = to(COLORS.BLACK, space);
  		black.alpha = origin.alpha;
  		return to(black, space);
  	}

  	if (inGamut(origin_OKLCH, space, { epsilon: 0 })) {
  		return to(origin_OKLCH, space);
  	}

  	function clip (_color) {
  		const destColor = to(_color, space);
  		const spaceCoords = Object.values(/** @type {ColorSpace} */ (space).coords);
  		destColor.coords = /** @type {[number, number, number]} */ (
  			destColor.coords.map((coord, index) => {
  				if ("range" in spaceCoords[index]) {
  					const [min, max] = spaceCoords[index].range;
  					return clamp(min, coord, max);
  				}
  				return coord;
  			})
  		);
  		return destColor;
  	}
  	let min = 0;
  	let max = origin_OKLCH.coords[1];
  	let min_inGamut = true;
  	let current = clone$1(origin_OKLCH);
  	let clipped = clip(current);

  	let E = deltaEOK(clipped, current);
  	if (E < JND) {
  		return clipped;
  	}

  	while (max - min > ε) {
  		const chroma = (min + max) / 2;
  		current.coords[1] = chroma;
  		if (min_inGamut && inGamut(current, space, { epsilon: 0 })) {
  			min = chroma;
  		}
  		else {
  			clipped = clip(current);
  			E = deltaEOK(clipped, current);
  			if (E < JND) {
  				if (JND - E < ε) {
  					break;
  				}
  				else {
  					min_inGamut = false;
  					min = chroma;
  				}
  			}
  			else {
  				max = chroma;
  			}
  		}
  	}
  	return clipped;
  }

  /** @import { ColorTypes, PlainColorObject, ToGamutOptions } from "./types.js" */

  /**
   * Convert to color space and return a new color
   * @param {ColorTypes} color
   * @param {string | ColorSpace} space
   * @param {{ inGamut?: boolean | ToGamutOptions | undefined }} options
   * @returns {PlainColorObject}
   */
  function to (color, space, { inGamut } = {}) {
  	color = getColor(color);
  	space = ColorSpace.get(space);

  	let coords = space.from(color);
  	let ret = { space, coords, alpha: color.alpha };

  	if (inGamut) {
  		ret = toGamut(ret, inGamut === true ? undefined : inGamut);
  	}

  	return ret;
  }

  /** @type {"color"} */
  to.returns = "color";

  // @ts-nocheck


  /** @import { ColorTypes, ParseOptions, PlainColorObject } from "./types.js" */

  // Type re-exports
  /** @typedef {import("./types.js").SerializeOptions} SerializeOptions */

  /**
   * Generic toString() method, outputs a color(spaceId ...coords) function, a functional syntax, or custom formats defined by the color space
   * @param {ColorTypes} color
   * @param {SerializeOptions & Record<string, any>} options
   * @returns {string}
   */
  function serialize (color, options = {}) {
  	let {
  		precision = defaults$1.precision,
  		format,
  		inGamut: inGamut$1 = true,
  		coords: coordFormat,
  		alpha: alphaFormat,
  		commas,
  	} = options;
  	let ret;

  	let colorWithMeta = /** @type {PlainColorObject & ParseOptions} */ (getColor(color));

  	let formatId = format;
  	let parseMeta = colorWithMeta.parseMeta;

  	if (parseMeta && !format) {
  		if (parseMeta.format.canSerialize()) {
  			format = parseMeta.format;
  			formatId = parseMeta.formatId;
  		}

  		coordFormat ??= parseMeta.types;
  		alphaFormat ??= parseMeta.alphaType;
  		commas ??= parseMeta.commas;
  	}

  	if (formatId) {
  		// A format is explicitly specified
  		format = colorWithMeta.space.getFormat(format) ?? ColorSpace.findFormat(formatId);
  	}

  	if (!format) {
  		// No format specified, or format not found
  		format = colorWithMeta.space.getFormat("default") ?? ColorSpace.DEFAULT_FORMAT;
  		formatId = format.name;
  	}

  	if (format && format.space && format.space !== colorWithMeta.space) {
  		// Format specified belongs to a different color space,
  		// need to convert to it first
  		colorWithMeta = to(colorWithMeta, format.space);
  	}

  	// The assignment to coords and inGamut needs to stay in the order they are now
  	// The order of the assignment was changed as a workaround for a bug in Next.js
  	// See this issue for details: https://github.com/color-js/color.js/issues/260

  	let coords = colorWithMeta.coords.slice(); // clone so we can manipulate it

  	inGamut$1 ||= format.toGamut;

  	if (inGamut$1 && !inGamut(colorWithMeta)) {
  		// FIXME what happens if the color contains none values?
  		coords = toGamut(clone$1(colorWithMeta), inGamut$1 === true ? undefined : inGamut$1).coords;
  	}

  	if (format.type === "custom") {
  		if (format.serialize) {
  			ret = format.serialize(coords, colorWithMeta.alpha, options);
  		}
  		else {
  			throw new TypeError(
  				`format ${formatId} can only be used to parse colors, not for serialization`,
  			);
  		}
  	}
  	else {
  		// Functional syntax
  		let name = format.name || "color";

  		let args = format.serializeCoords(coords, precision, coordFormat);

  		if (name === "color") {
  			// If output is a color() function, add colorspace id as first argument
  			let cssId =
  				format.id || format.ids?.[0] || colorWithMeta.space.cssId || colorWithMeta.space.id;
  			args.unshift(cssId);
  		}

  		// Serialize alpha?
  		/** @type {string | number} */
  		let alpha = colorWithMeta.alpha;

  		if (alphaFormat !== undefined && !(typeof alphaFormat === "object")) {
  			alphaFormat =
  				typeof alphaFormat === "string" ? { type: alphaFormat } : { include: alphaFormat };
  		}

  		let alphaType = alphaFormat?.type ?? "<number>";
  		let serializeAlpha =
  			alphaFormat?.include === true ||
  			format.alpha === true ||
  			(alphaFormat?.include !== false && format.alpha !== false && alpha < 1);
  		let strAlpha = "";

  		commas ??= format.commas;

  		if (serializeAlpha) {
  			if (precision !== null) {
  				let unit;

  				if (alphaType === "<percentage>") {
  					unit = "%";
  					alpha *= 100;
  				}

  				alpha = serializeNumber(alpha, { precision, unit });
  			}

  			strAlpha = `${commas ? "," : " /"} ${alpha}`;
  		}

  		ret = `${name}(${args.join(commas ? ", " : " ")}${strAlpha})`;
  	}

  	return ret;
  }

  /** @import { Matrix3x3 } from "../types.js" */

  /** @type {Matrix3x3} */
  // prettier-ignore
  const toXYZ_M$1 = [
  	[0.4865709486482162, 0.26566769316909306, 0.1982172852343625],
  	[0.2289745640697488, 0.6917385218365064,  0.079286914093745],
  	[0.0000000000000000, 0.04511338185890264, 1.043944368900976],
  ];

  /** @type {Matrix3x3} */
  // prettier-ignore
  const fromXYZ_M$1 = [
  	[ 2.493496911941425,   -0.9313836179191239, -0.40271078445071684],
  	[-0.8294889695615747,   1.7626640603183463,  0.023624685841943577],
  	[ 0.03584583024378447, -0.07617238926804182, 0.9568845240076872],
  ];

  var P3Linear = new RGBColorSpace({
  	id: "p3-linear",
  	cssId: "display-p3-linear",
  	name: "Linear P3",
  	white: "D65",
  	toXYZ_M: toXYZ_M$1,
  	fromXYZ_M: fromXYZ_M$1,
  });

  /** @import { Matrix3x3 } from "../types.js" */

  // This is the linear-light version of sRGB
  // as used for example in SVG filters
  // or in Canvas

  // This matrix was calculated directly from the RGB and white chromaticities
  // when rounded to 8 decimal places, it agrees completely with the official matrix
  // see https://github.com/w3c/csswg-drafts/issues/5922
  /** @type {Matrix3x3} */
  // prettier-ignore
  const toXYZ_M = [
  	[ 0.41239079926595934, 0.357584339383878,   0.1804807884018343  ],
  	[ 0.21263900587151027, 0.715168678767756,   0.07219231536073371 ],
  	[ 0.01933081871559182, 0.11919477979462598, 0.9505321522496607  ],
  ];

  // This matrix is the inverse of the above;
  // again it agrees with the official definition when rounded to 8 decimal places
  /** @type {Matrix3x3} */
  // prettier-ignore
  const fromXYZ_M = [
  	[  3.2409699419045226,  -1.537383177570094,   -0.4986107602930034  ],
  	[ -0.9692436362808796,   1.8759675015077202,   0.04155505740717559 ],
  	[  0.05563007969699366, -0.20397695888897652,  1.0569715142428786  ],
  ];

  var sRGBLinear = new RGBColorSpace({
  	id: "srgb-linear",
  	name: "Linear sRGB",
  	white: "D65",
  	toXYZ_M,
  	fromXYZ_M,
  });

  // To produce: Visit https://www.w3.org/TR/css-color-4/#named-colors
  // and run in the console:
  // copy($$("tr", $(".named-color-table tbody")).map(tr => `"${tr.cells[2].textContent.trim()}": [${tr.cells[4].textContent.trim().split(/\s+/).map(c => c === "0"? "0" : c === "255"? "1" : c + " / 255").join(", ")}]`).join(",\n"))

  /** List of CSS color keywords
   *  Note that this does not include currentColor, transparent,
   *  or system colors
   *
   *  @type {Record<string, [number, number, number]>}
   */
  var KEYWORDS = {
  	aliceblue: [240 / 255, 248 / 255, 1],
  	antiquewhite: [250 / 255, 235 / 255, 215 / 255],
  	aqua: [0, 1, 1],
  	aquamarine: [127 / 255, 1, 212 / 255],
  	azure: [240 / 255, 1, 1],
  	beige: [245 / 255, 245 / 255, 220 / 255],
  	bisque: [1, 228 / 255, 196 / 255],
  	black: [0, 0, 0],
  	blanchedalmond: [1, 235 / 255, 205 / 255],
  	blue: [0, 0, 1],
  	blueviolet: [138 / 255, 43 / 255, 226 / 255],
  	brown: [165 / 255, 42 / 255, 42 / 255],
  	burlywood: [222 / 255, 184 / 255, 135 / 255],
  	cadetblue: [95 / 255, 158 / 255, 160 / 255],
  	chartreuse: [127 / 255, 1, 0],
  	chocolate: [210 / 255, 105 / 255, 30 / 255],
  	coral: [1, 127 / 255, 80 / 255],
  	cornflowerblue: [100 / 255, 149 / 255, 237 / 255],
  	cornsilk: [1, 248 / 255, 220 / 255],
  	crimson: [220 / 255, 20 / 255, 60 / 255],
  	cyan: [0, 1, 1],
  	darkblue: [0, 0, 139 / 255],
  	darkcyan: [0, 139 / 255, 139 / 255],
  	darkgoldenrod: [184 / 255, 134 / 255, 11 / 255],
  	darkgray: [169 / 255, 169 / 255, 169 / 255],
  	darkgreen: [0, 100 / 255, 0],
  	darkgrey: [169 / 255, 169 / 255, 169 / 255],
  	darkkhaki: [189 / 255, 183 / 255, 107 / 255],
  	darkmagenta: [139 / 255, 0, 139 / 255],
  	darkolivegreen: [85 / 255, 107 / 255, 47 / 255],
  	darkorange: [1, 140 / 255, 0],
  	darkorchid: [153 / 255, 50 / 255, 204 / 255],
  	darkred: [139 / 255, 0, 0],
  	darksalmon: [233 / 255, 150 / 255, 122 / 255],
  	darkseagreen: [143 / 255, 188 / 255, 143 / 255],
  	darkslateblue: [72 / 255, 61 / 255, 139 / 255],
  	darkslategray: [47 / 255, 79 / 255, 79 / 255],
  	darkslategrey: [47 / 255, 79 / 255, 79 / 255],
  	darkturquoise: [0, 206 / 255, 209 / 255],
  	darkviolet: [148 / 255, 0, 211 / 255],
  	deeppink: [1, 20 / 255, 147 / 255],
  	deepskyblue: [0, 191 / 255, 1],
  	dimgray: [105 / 255, 105 / 255, 105 / 255],
  	dimgrey: [105 / 255, 105 / 255, 105 / 255],
  	dodgerblue: [30 / 255, 144 / 255, 1],
  	firebrick: [178 / 255, 34 / 255, 34 / 255],
  	floralwhite: [1, 250 / 255, 240 / 255],
  	forestgreen: [34 / 255, 139 / 255, 34 / 255],
  	fuchsia: [1, 0, 1],
  	gainsboro: [220 / 255, 220 / 255, 220 / 255],
  	ghostwhite: [248 / 255, 248 / 255, 1],
  	gold: [1, 215 / 255, 0],
  	goldenrod: [218 / 255, 165 / 255, 32 / 255],
  	gray: [128 / 255, 128 / 255, 128 / 255],
  	green: [0, 128 / 255, 0],
  	greenyellow: [173 / 255, 1, 47 / 255],
  	grey: [128 / 255, 128 / 255, 128 / 255],
  	honeydew: [240 / 255, 1, 240 / 255],
  	hotpink: [1, 105 / 255, 180 / 255],
  	indianred: [205 / 255, 92 / 255, 92 / 255],
  	indigo: [75 / 255, 0, 130 / 255],
  	ivory: [1, 1, 240 / 255],
  	khaki: [240 / 255, 230 / 255, 140 / 255],
  	lavender: [230 / 255, 230 / 255, 250 / 255],
  	lavenderblush: [1, 240 / 255, 245 / 255],
  	lawngreen: [124 / 255, 252 / 255, 0],
  	lemonchiffon: [1, 250 / 255, 205 / 255],
  	lightblue: [173 / 255, 216 / 255, 230 / 255],
  	lightcoral: [240 / 255, 128 / 255, 128 / 255],
  	lightcyan: [224 / 255, 1, 1],
  	lightgoldenrodyellow: [250 / 255, 250 / 255, 210 / 255],
  	lightgray: [211 / 255, 211 / 255, 211 / 255],
  	lightgreen: [144 / 255, 238 / 255, 144 / 255],
  	lightgrey: [211 / 255, 211 / 255, 211 / 255],
  	lightpink: [1, 182 / 255, 193 / 255],
  	lightsalmon: [1, 160 / 255, 122 / 255],
  	lightseagreen: [32 / 255, 178 / 255, 170 / 255],
  	lightskyblue: [135 / 255, 206 / 255, 250 / 255],
  	lightslategray: [119 / 255, 136 / 255, 153 / 255],
  	lightslategrey: [119 / 255, 136 / 255, 153 / 255],
  	lightsteelblue: [176 / 255, 196 / 255, 222 / 255],
  	lightyellow: [1, 1, 224 / 255],
  	lime: [0, 1, 0],
  	limegreen: [50 / 255, 205 / 255, 50 / 255],
  	linen: [250 / 255, 240 / 255, 230 / 255],
  	magenta: [1, 0, 1],
  	maroon: [128 / 255, 0, 0],
  	mediumaquamarine: [102 / 255, 205 / 255, 170 / 255],
  	mediumblue: [0, 0, 205 / 255],
  	mediumorchid: [186 / 255, 85 / 255, 211 / 255],
  	mediumpurple: [147 / 255, 112 / 255, 219 / 255],
  	mediumseagreen: [60 / 255, 179 / 255, 113 / 255],
  	mediumslateblue: [123 / 255, 104 / 255, 238 / 255],
  	mediumspringgreen: [0, 250 / 255, 154 / 255],
  	mediumturquoise: [72 / 255, 209 / 255, 204 / 255],
  	mediumvioletred: [199 / 255, 21 / 255, 133 / 255],
  	midnightblue: [25 / 255, 25 / 255, 112 / 255],
  	mintcream: [245 / 255, 1, 250 / 255],
  	mistyrose: [1, 228 / 255, 225 / 255],
  	moccasin: [1, 228 / 255, 181 / 255],
  	navajowhite: [1, 222 / 255, 173 / 255],
  	navy: [0, 0, 128 / 255],
  	oldlace: [253 / 255, 245 / 255, 230 / 255],
  	olive: [128 / 255, 128 / 255, 0],
  	olivedrab: [107 / 255, 142 / 255, 35 / 255],
  	orange: [1, 165 / 255, 0],
  	orangered: [1, 69 / 255, 0],
  	orchid: [218 / 255, 112 / 255, 214 / 255],
  	palegoldenrod: [238 / 255, 232 / 255, 170 / 255],
  	palegreen: [152 / 255, 251 / 255, 152 / 255],
  	paleturquoise: [175 / 255, 238 / 255, 238 / 255],
  	palevioletred: [219 / 255, 112 / 255, 147 / 255],
  	papayawhip: [1, 239 / 255, 213 / 255],
  	peachpuff: [1, 218 / 255, 185 / 255],
  	peru: [205 / 255, 133 / 255, 63 / 255],
  	pink: [1, 192 / 255, 203 / 255],
  	plum: [221 / 255, 160 / 255, 221 / 255],
  	powderblue: [176 / 255, 224 / 255, 230 / 255],
  	purple: [128 / 255, 0, 128 / 255],
  	rebeccapurple: [102 / 255, 51 / 255, 153 / 255],
  	red: [1, 0, 0],
  	rosybrown: [188 / 255, 143 / 255, 143 / 255],
  	royalblue: [65 / 255, 105 / 255, 225 / 255],
  	saddlebrown: [139 / 255, 69 / 255, 19 / 255],
  	salmon: [250 / 255, 128 / 255, 114 / 255],
  	sandybrown: [244 / 255, 164 / 255, 96 / 255],
  	seagreen: [46 / 255, 139 / 255, 87 / 255],
  	seashell: [1, 245 / 255, 238 / 255],
  	sienna: [160 / 255, 82 / 255, 45 / 255],
  	silver: [192 / 255, 192 / 255, 192 / 255],
  	skyblue: [135 / 255, 206 / 255, 235 / 255],
  	slateblue: [106 / 255, 90 / 255, 205 / 255],
  	slategray: [112 / 255, 128 / 255, 144 / 255],
  	slategrey: [112 / 255, 128 / 255, 144 / 255],
  	snow: [1, 250 / 255, 250 / 255],
  	springgreen: [0, 1, 127 / 255],
  	steelblue: [70 / 255, 130 / 255, 180 / 255],
  	tan: [210 / 255, 180 / 255, 140 / 255],
  	teal: [0, 128 / 255, 128 / 255],
  	thistle: [216 / 255, 191 / 255, 216 / 255],
  	tomato: [1, 99 / 255, 71 / 255],
  	turquoise: [64 / 255, 224 / 255, 208 / 255],
  	violet: [238 / 255, 130 / 255, 238 / 255],
  	wheat: [245 / 255, 222 / 255, 179 / 255],
  	white: [1, 1, 1],
  	whitesmoke: [245 / 255, 245 / 255, 245 / 255],
  	yellow: [1, 1, 0],
  	yellowgreen: [154 / 255, 205 / 255, 50 / 255],
  };

  /** @import { Coords } from "../types.js" */

  let coordGrammar = Array(3).fill("<percentage> | <number>[0, 255]");
  let coordGrammarNumber = Array(3).fill("<number>[0, 255]");

  var sRGB = new RGBColorSpace({
  	id: "srgb",
  	name: "sRGB",
  	base: sRGBLinear,
  	fromBase: rgb => {
  		// convert an array of linear-light sRGB values in the range 0.0-1.0
  		// to gamma corrected form
  		// https://en.wikipedia.org/wiki/SRGB
  		return rgb.map(val => {
  			let sign = val < 0 ? -1 : 1;
  			let abs = val * sign;

  			if (abs > 0.0031308) {
  				return sign * (1.055 * abs ** (1 / 2.4) - 0.055);
  			}

  			return 12.92 * val;
  		});
  	},
  	toBase: rgb => {
  		// convert an array of sRGB values in the range 0.0 - 1.0
  		// to linear light (un-companded) form.
  		// https://en.wikipedia.org/wiki/SRGB
  		return rgb.map(val => {
  			let sign = val < 0 ? -1 : 1;
  			let abs = val * sign;

  			if (abs <= 0.04045) {
  				return val / 12.92;
  			}

  			return sign * ((abs + 0.055) / 1.055) ** 2.4;
  		});
  	},
  	formats: {
  		rgb: {
  			coords: coordGrammar,
  		},
  		rgb_number: {
  			name: "rgb",
  			commas: true,
  			coords: coordGrammarNumber,
  			alpha: false,
  		},
  		color: {
  			/* use defaults */
  		},
  		rgba: {
  			coords: coordGrammar,
  			commas: true,
  			alpha: true,
  		},
  		rgba_number: {
  			name: "rgba",
  			commas: true,
  			coords: coordGrammarNumber,
  		},
  		hex: {
  			type: "custom",
  			toGamut: true,
  			test: str => /^#(([a-f0-9]{2}){3,4}|[a-f0-9]{3,4})$/i.test(str),
  			parse (str) {
  				if (str.length <= 5) {
  					// #rgb or #rgba, duplicate digits
  					str = str.replace(/[a-f0-9]/gi, "$&$&");
  				}

  				/** @type {number[]} */
  				let rgba = [];
  				// @ts-expect-error Type 'void' is not assignable to type 'string'
  				str.replace(/[a-f0-9]{2}/gi, component => {
  					rgba.push(parseInt(component, 16) / 255);
  				});

  				return {
  					spaceId: "srgb",
  					coords: /** @type {Coords} */ (rgba.slice(0, 3)),
  					alpha: /** @type {number} */ (rgba.slice(3)[0]),
  				};
  			},
  			serialize: (
  				coords,
  				alpha,
  				{
  					collapse = true, // collapse to 3-4 digit hex when possible?
  					alpha: alphaFormat,
  				} = {},
  			) => {
  				if ((alphaFormat !== false && alpha < 1) || alphaFormat === true) {
  					coords.push(alpha);
  				}

  				coords = /** @type {[number, number, number]} */ (
  					coords.map(c => Math.round(c * 255))
  				);

  				let collapsible = collapse && coords.every(c => c % 17 === 0);

  				let hex = coords
  					.map(c => {
  						if (collapsible) {
  							return (c / 17).toString(16);
  						}

  						return c.toString(16).padStart(2, "0");
  					})
  					.join("");

  				return "#" + hex;
  			},
  		},
  		keyword: {
  			type: "custom",
  			test: str => /^[a-z]+$/i.test(str),
  			parse (str) {
  				str = str.toLowerCase();
  				let ret = { spaceId: "srgb", coords: null, alpha: 1 };

  				if (str === "transparent") {
  					ret.coords = KEYWORDS.black;
  					ret.alpha = 0;
  				}
  				else {
  					ret.coords = KEYWORDS[str];
  				}

  				if (ret.coords) {
  					return ret;
  				}
  			},
  		},
  	},
  });

  var P3 = new RGBColorSpace({
  	id: "p3",
  	cssId: "display-p3",
  	name: "P3",
  	base: P3Linear,
  	// Gamma encoding/decoding is the same as sRGB
  	fromBase: sRGB.fromBase,
  	toBase: sRGB.toBase,
  });

  /**
   * Relative luminance
   */

  /** @import { ColorTypes } from "./types.js" */

  /**
   *
   * @param {ColorTypes} color
   * @returns {number}
   */
  function getLuminance (color) {
  	// Assume getColor() is called on color in get()
  	return get$1(color, [xyz_d65, "y"]);
  }

  // WCAG 2.0 contrast https://www.w3.org/TR/WCAG20-TECHS/G18.html
  // Simple contrast, with fixed 5% viewing flare contribution
  // Symmetric, does not matter which is foreground and which is background


  /**
   * @param {import("../types.js").ColorTypes} color1
   * @param {import("../types.js").ColorTypes} color2
   * @returns {number}
   */
  function contrastWCAG21 (color1, color2) {
  	color1 = getColor(color1);
  	color2 = getColor(color2);

  	let Y1 = Math.max(getLuminance(color1), 0);
  	let Y2 = Math.max(getLuminance(color2), 0);

  	if (Y2 > Y1) {
  		[Y1, Y2] = [Y2, Y1];
  	}

  	return (Y1 + 0.05) / (Y2 + 0.05);
  }

  // APCA 0.0.98G
  // https://github.com/Myndex/apca-w3
  // see also https://github.com/w3c/silver/issues/643


  // exponents
  const normBG = 0.56;
  const normTXT = 0.57;
  const revTXT = 0.62;
  const revBG = 0.65;

  // clamps
  const blkThrs = 0.022;
  const blkClmp = 1.414;
  const loClip = 0.1;
  const deltaYmin = 0.0005;

  // scalers
  // see https://github.com/w3c/silver/issues/645
  const scaleBoW = 1.14;
  const loBoWoffset = 0.027;
  const scaleWoB = 1.14;

  function fclamp (Y) {
  	if (Y >= blkThrs) {
  		return Y;
  	}
  	return Y + (blkThrs - Y) ** blkClmp;
  }

  function linearize (val) {
  	let sign = val < 0 ? -1 : 1;
  	let abs = Math.abs(val);
  	return sign * Math.pow(abs, 2.4);
  }

  /**
   * Not symmetric, requires a foreground (text) color, and a background color
   * @param {import("../types.js").ColorTypes} background
   * @param {import("../types.js").ColorTypes} foreground
   * @returns {number}
   */
  function contrastAPCA (background, foreground) {
  	foreground = getColor(foreground);
  	background = getColor(background);

  	let S;
  	let C;
  	let Sapc;

  	// Myndex as-published, assumes sRGB inputs
  	let R, G, B;

  	foreground = to(foreground, "srgb");
  	// Should these be clamped to in-gamut values?

  	// Calculates "screen luminance" with non-standard simple gamma EOTF
  	// weights should be from CSS Color 4, not the ones here which are via Myndex and copied from Lindbloom
  	[R, G, B] = foreground.coords.map(c => {
  		return isNone(c) ? 0 : c;
  	});
  	let lumTxt = linearize(R) * 0.2126729 + linearize(G) * 0.7151522 + linearize(B) * 0.072175;

  	background = to(background, "srgb");
  	[R, G, B] = background.coords.map(c => {
  		return isNone(c) ? 0 : c;
  	});
  	let lumBg = linearize(R) * 0.2126729 + linearize(G) * 0.7151522 + linearize(B) * 0.072175;

  	// toe clamping of very dark values to account for flare
  	let Ytxt = fclamp(lumTxt);
  	let Ybg = fclamp(lumBg);

  	// are we "Black on White" (dark on light), or light on dark?
  	let BoW = Ybg > Ytxt;

  	// why is this a delta, when Y is not perceptually uniform?
  	// Answer: it is a noise gate, see
  	// https://github.com/LeaVerou/color.js/issues/208
  	if (Math.abs(Ybg - Ytxt) < deltaYmin) {
  		C = 0;
  	}
  	else {
  		if (BoW) {
  			// dark text on light background
  			S = Ybg ** normBG - Ytxt ** normTXT;
  			C = S * scaleBoW;
  		}
  		else {
  			// light text on dark background
  			S = Ybg ** revBG - Ytxt ** revTXT;
  			C = S * scaleWoB;
  		}
  	}
  	if (Math.abs(C) < loClip) {
  		Sapc = 0;
  	}
  	else if (C > 0) {
  		// not clear whether Woffset is loBoWoffset or loWoBoffset
  		// but they have the same value
  		Sapc = C - loBoWoffset;
  	}
  	else {
  		Sapc = C + loBoWoffset;
  	}

  	return Sapc * 100;
  }

  /**
   * Functions related to color interpolation
   */

  /**
   * Creates a function that accepts a number and returns a color.
   * For numbers in the range 0 to 1, the function interpolates;
   * for numbers outside that range, the function extrapolates
   * (and thus may not return the results you expect)
   * @overload
   * @param {Range} range
   * @param {RangeOptions} [options]
   * @returns {Range}
   */
  /**
   * @overload
   * @param {ColorTypes} color1
   * @param {ColorTypes} color2
   * @param {RangeOptions & Record<string, any>} [options]
   * @returns {Range}
   */
  function range (color1, color2, options = {}) {
  	if (isRange(color1)) {
  		// Tweaking existing range
  		let [r, options] = [color1, color2];

  		return range(...r.rangeArgs.colors, { ...r.rangeArgs.options, ...options });
  	}

  	let { space, outputSpace, progression, premultiplied } = options;

  	color1 = getColor(color1);
  	color2 = getColor(color2);

  	// Make sure we're working on copies of these colors
  	color1 = clone$1(color1);
  	color2 = clone$1(color2);

  	let rangeArgs = { colors: [color1, color2], options };

  	if (space) {
  		space = ColorSpace.get(space);
  	}
  	else {
  		space = ColorSpace.registry[defaults$1.interpolationSpace] || color1.space;
  	}

  	outputSpace = outputSpace ? ColorSpace.get(outputSpace) : space;

  	color1 = to(color1, space);
  	color2 = to(color2, space);

  	// Gamut map to avoid areas of flat color
  	color1 = toGamut(color1);
  	color2 = toGamut(color2);

  	// Handle hue interpolation
  	// See https://github.com/w3c/csswg-drafts/issues/4735#issuecomment-635741840
  	if (space.coords.h && space.coords.h.type === "angle") {
  		let arc = (options.hue = options.hue || "shorter");

  		let /** @type {Ref} */ hue = [space, "h"];
  		let [θ1, θ2] = [get$1(color1, hue), get$1(color2, hue)];
  		// Undefined hues must be evaluated before hue fix-up to properly
  		// calculate hue arcs between undefined and defined hues.
  		// See https://github.com/w3c/csswg-drafts/issues/9436#issuecomment-1746957545
  		if (isNone(θ1) && !isNone(θ2)) {
  			θ1 = θ2;
  		}
  		else if (isNone(θ2) && !isNone(θ1)) {
  			θ2 = θ1;
  		}
  		[θ1, θ2] = adjust(arc, [θ1, θ2]);
  		set(color1, hue, θ1);
  		set(color2, hue, θ2);
  	}

  	if (premultiplied) {
  		// not coping with polar spaces yet
  		color1.coords = /** @type {[number, number, number]} */ (
  			color1.coords.map(c => c * color1.alpha)
  		);
  		color2.coords = /** @type {[number, number, number]} */ (
  			color2.coords.map(c => c * color2.alpha)
  		);
  	}

  	return Object.assign(
  		p => {
  			p = progression ? progression(p) : p;
  			let coords = color1.coords.map((start, i) => {
  				let end = color2.coords[i];
  				return interpolate(start, end, p);
  			});

  			let alpha = interpolate(color1.alpha, color2.alpha, p);
  			let ret = { space, coords, alpha };

  			if (premultiplied) {
  				// undo premultiplication
  				ret.coords = ret.coords.map(c => c / alpha);
  			}

  			if (outputSpace !== space) {
  				ret = to(ret, outputSpace);
  			}

  			return ret;
  		},
  		{
  			rangeArgs,
  		},
  	);
  }

  /**
   * @param {any} val
   * @returns {val is Range}
   */
  function isRange (val) {
  	return type$1(val) === "function" && !!val.rangeArgs;
  }

  defaults$1.interpolationSpace = "lab";

  var HSLSpace = new ColorSpace({
  	id: "hsl",
  	name: "HSL",
  	coords: {
  		h: {
  			refRange: [0, 360],
  			type: "angle",
  			name: "Hue",
  		},
  		s: {
  			range: [0, 100],
  			name: "Saturation",
  		},
  		l: {
  			range: [0, 100],
  			name: "Lightness",
  		},
  	},

  	base: sRGB,

  	// Adapted from https://drafts.csswg.org/css-color-4/better-rgbToHsl.js
  	fromBase: rgb => {
  		let max = Math.max(...rgb);
  		let min = Math.min(...rgb);
  		let [r, g, b] = rgb;
  		let [h, s, l] = [null, 0, (min + max) / 2];
  		let d = max - min;

  		if (d !== 0) {
  			s = l === 0 || l === 1 ? 0 : (max - l) / Math.min(l, 1 - l);

  			switch (max) {
  				case r:
  					h = (g - b) / d + (g < b ? 6 : 0);
  					break;
  				case g:
  					h = (b - r) / d + 2;
  					break;
  				case b:
  					h = (r - g) / d + 4;
  			}

  			h = h * 60;
  		}

  		// Very out of gamut colors can produce negative saturation
  		// If so, just rotate the hue by 180 and use a positive saturation
  		// see https://github.com/w3c/csswg-drafts/issues/9222
  		if (s < 0) {
  			h += 180;
  			s = Math.abs(s);
  		}

  		if (h >= 360) {
  			h -= 360;
  		}

  		return [h, s * 100, l * 100];
  	},

  	// Adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB_alternative
  	toBase: hsl => {
  		let [h, s, l] = hsl;
  		h = h % 360;

  		if (h < 0) {
  			h += 360;
  		}

  		s /= 100;
  		l /= 100;

  		function f (n) {
  			let k = (n + h / 30) % 12;
  			let a = s * Math.min(l, 1 - l);
  			return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
  		}

  		return [f(0), f(8), f(4)];
  	},

  	formats: {
  		hsl: {
  			coords: ["<number> | <angle>", "<percentage> | <number>", "<percentage> | <number>"],
  		},
  		hsla: {
  			coords: ["<number> | <angle>", "<percentage> | <number>", "<percentage> | <number>"],
  			commas: true,
  			alpha: true,
  		},
  	},
  });

  // Note that, like HSL, calculations are done directly on
  // gamma-corrected sRGB values rather than linearising them first.

  var HSV = new ColorSpace({
  	id: "hsv",
  	name: "HSV",
  	coords: {
  		h: {
  			refRange: [0, 360],
  			type: "angle",
  			name: "Hue",
  		},
  		s: {
  			range: [0, 100],
  			name: "Saturation",
  		},
  		v: {
  			range: [0, 100],
  			name: "Value",
  		},
  	},

  	base: sRGB,
  	// https://en.wikipedia.org/wiki/HSL_and_HSV#Formal_derivation
  	fromBase (rgb) {
  		let max = Math.max(...rgb);
  		let min = Math.min(...rgb);
  		let [r, g, b] = rgb;
  		let [h, s, v] = [null, 0, max];
  		let d = max - min;

  		if (d !== 0) {
  			switch (max) {
  				case r:
  					h = (g - b) / d + (g < b ? 6 : 0);
  					break;
  				case g:
  					h = (b - r) / d + 2;
  					break;
  				case b:
  					h = (r - g) / d + 4;
  			}

  			h = h * 60;
  		}

  		if (v) {
  			s = d / v;
  		}

  		if (h >= 360) {
  			h -= 360;
  		}

  		return [h, s * 100, v * 100];
  	},
  	// Adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative
  	toBase (hsv) {
  		let [h, s, v] = hsv;
  		h = h % 360;

  		if (h < 0) {
  			h += 360;
  		}

  		s /= 100;
  		v /= 100;

  		function f (n) {
  			let k = (n + h / 60) % 6;
  			return v - v * s * Math.max(0, Math.min(k, 4 - k, 1));
  		}

  		return [f(5), f(3), f(1)];
  	},

  	formats: {
  		color: {
  			id: "--hsv",
  			coords: ["<number> | <angle>", "<percentage> | <number>", "<percentage> | <number>"],
  		},
  	},
  });

  // The Hue, Whiteness Blackness (HWB) colorspace
  // See https://drafts.csswg.org/css-color-4/#the-hwb-notation
  // Note that, like HSL, calculations are done directly on
  // gamma-corrected sRGB values rather than linearising them first.

  var HWBSpace = new ColorSpace({
  	id: "hwb",
  	name: "HWB",
  	coords: {
  		h: {
  			refRange: [0, 360],
  			type: "angle",
  			name: "Hue",
  		},
  		w: {
  			range: [0, 100],
  			name: "Whiteness",
  		},
  		b: {
  			range: [0, 100],
  			name: "Blackness",
  		},
  	},

  	base: HSV,
  	fromBase (hsv) {
  		let [h, s, v] = hsv;

  		return [h, (v * (100 - s)) / 100, 100 - v];
  	},
  	toBase (hwb) {
  		let [h, w, b] = hwb;

  		// Now convert percentages to [0..1]
  		w /= 100;
  		b /= 100;

  		// Achromatic check (white plus black >= 1)
  		let sum = w + b;
  		if (sum >= 1) {
  			let gray = w / sum;
  			return [h, 0, gray * 100];
  		}

  		let v = 1 - b;
  		let s = v === 0 ? 0 : 1 - w / v;
  		return [h, s * 100, v * 100];
  	},

  	formats: {
  		hwb: {
  			coords: ["<number> | <angle>", "<percentage> | <number>", "<percentage> | <number>"],
  		},
  	},
  });

  var OKLCHSpace = new ColorSpace({
  	id: "oklch",
  	name: "OkLCh",
  	coords: {
  		l: {
  			refRange: [0, 1],
  			name: "Lightness",
  		},
  		c: {
  			refRange: [0, 0.4],
  			name: "Chroma",
  		},
  		h: {
  			refRange: [0, 360],
  			type: "angle",
  			name: "Hue",
  		},
  	},
  	white: "D65",

  	base: OKLab,
  	fromBase: LCHSpace.fromBase,
  	toBase: LCHSpace.toBase,

  	formats: {
  		oklch: {
  			coords: ["<percentage> | <number>", "<number> | <percentage>", "<number> | <angle>"],
  		},
  	},
  });

  var HSBSpace = new ColorSpace({
    id: 'hsb',
    name: 'HSB',
    coords: {
      h: {
        refRange: [0, 360],
        type: 'angle',
        name: 'Hue'
      },
      s: {
        range: [0, 100],
        name: 'Saturation'
      },
      b: {
        range: [0, 100],
        name: 'Brightness'
      }
    },

    base: sRGB,

    fromBase: rgb => {
      const val = Math.max(...rgb);
      const chroma = val - Math.min(...rgb);

      let [red, green, blue] = rgb;

      let hue, sat;
      if (chroma === 0) {
        // Return early if grayscale.
        hue = 0;
        sat = 0;
      } else {
        sat = chroma / val;
        if (red === val) {
          // Magenta to yellow.
          hue = (green - blue) / chroma;
        } else if (green === val) {
          // Yellow to cyan.
          hue = 2 + (blue - red) / chroma;
        } else if (blue === val) {
          // Cyan to magenta.
          hue = 4 + (red - green) / chroma;
        }
        if (hue < 0) {
          // Confine hue to the interval [0, 1).
          hue += 6;
        } else if (hue >= 6) {
          hue -= 6;
        }
      }

      return [hue / 6 * 360, sat * 100, val * 100];
    },
    toBase,

    formats: {
      default: {
        type: 'custom',
        serialize: (coords, alpha) => {
          const rgb = toBase(coords);
          let ret = `rgb(${
          Math.round(rgb[0] * 100 * 100) / 100
        }% ${
          Math.round(rgb[1] * 100 * 100) / 100
        }% ${
          Math.round(rgb[2] * 100 * 100) / 100
        }%`;

          if (alpha < 1) {
            ret += ` / ${alpha}`;
          }

          ret += ')';

          return ret;
        }
      },
      'hsb': {
        coords: ['<number> | <angle>', '<percentage>', '<percentage>']
      },
      'hsba': {
        coords: ['<number> | <angle>', '<percentage>', '<percentage>'],
        commans: true,
        lastAlpha: true
      }
    }
  });

  function toBase(hsb){
    const hue = hsb[0] / 360 * 6; // We will split hue into 6 sectors.
    const sat = hsb[1] / 100;
    const val = hsb[2] / 100;

    let RGB = [];

    if (sat === 0) {
      RGB = [val, val, val]; // Return early if grayscale.
    } else {
      const sector = Math.floor(hue);
      const tint1 = val * (1 - sat);
      const tint2 = val * (1 - sat * (hue - sector));
      const tint3 = val * (1 - sat * (1 + sector - hue));
      let red, green, blue;
      if (sector === 1) {
        // Yellow to green.
        red = tint2;
        green = val;
        blue = tint1;
      } else if (sector === 2) {
        // Green to cyan.
        red = tint1;
        green = val;
        blue = tint3;
      } else if (sector === 3) {
        // Cyan to blue.
        red = tint1;
        green = tint2;
        blue = val;
      } else if (sector === 4) {
        // Blue to magenta.
        red = tint3;
        green = tint1;
        blue = val;
      } else if (sector === 5) {
        // Magenta to red.
        red = val;
        green = tint1;
        blue = tint2;
      } else {
        // Red to yellow (sector could be 0 or 6).
        red = val;
        green = tint3;
        blue = tint1;
      }
      RGB = [red, green, blue];
    }

    return RGB;
  }

  /**
   * @module Color
   * @submodule Creating & Reading
   * @for p5
   * @requires core
   * @requires color_conversion
   */


  const map = (n, start1, stop1, start2, stop2, clamp) => {
    let result = ((n - start1) / (stop1 - start1) * (stop2 - start2) + start2);
    if (clamp) {
      result = Math.max(result, Math.min(start2, stop2));
      result = Math.min(result, Math.max(start2, stop2));
    }
    return result;
  };

  const toHexComponent = (v) => {
    const vInt = ~~(v * 255);
    const hex = vInt.toString(16);
    if (hex.length < 2) {
      return '0' + hex
    } else {
      return hex
    }
  };

  const serializationMap = new Map();




  class Color {
    static colorMap = {};
    static #colorjsMaxes = {};
    static #grayscaleMap = {};

    // Used to add additional color modes to p5.js
    // Uses underlying library's definition
    static addColorMode(mode, definition){
      ColorSpace.register(definition);
      Color.colorMap[mode] = definition.id;

      // Get colorjs maxes
      Color.#colorjsMaxes[mode] = Object.values(definition.coords)
        .reduce((acc, v) => {
          acc.push(v.refRange || v.range);
          return acc;
        }, []);
      Color.#colorjsMaxes[mode].push([0, 1]);

      // Get grayscale mapping
      Color.#grayscaleMap[mode] = definition.fromGray;
    }

    constructor(vals, colorMode, colorMaxes, { clamp = false } = {}) {
      // This changes with the color object
      this._cachedMode = colorMode || RGB;

      if(vals instanceof Color){
        // Received Color object to be used for color mode conversion
        const mode = colorMode ?
          Color.colorMap[colorMode] :
          Color.colorMap[vals.mode];
        this._initialize = () => {
          this._cachedColor = to(vals._color, mode);
          this._cachedMode = mode;
        };

      }else if (typeof vals === 'object' && !Array.isArray(vals) && vals !== null){
        // Received color.js object to be used internally
        const mode = colorMode ?
          Color.colorMap[colorMode] :
          vals.spaceId;
        this._initialize = () => {
          this._cachedColor = to(vals, mode);
          this._cachedMode = colorMode || Object.entries(Color.colorMap)
            .find(([key, val]) => {
              return val === this._cachedColor.spaceId;
            });
        };

      } else if(typeof vals[0] === 'string') {
        // Received string
        this._defaultStringValue = vals[0];
        this._initialize = () => {
          try{
            this._cachedColor = parse$4(vals[0]);
            const [mode] = Object.entries(Color.colorMap).find(([key, val]) => {
              return val === this._cachedColor.spaceId;
            });
            this._cachedMode = mode;
            this._cachedColor = to(this._cachedColor, this._cachedColor.spaceId);
          }catch(err){
            // TODO: Invalid color string
            throw new Error('Invalid color string');
          }
        };

      }else {
        // Received individual channel values
        let mappedVals;

        if(colorMaxes){
          // NOTE: need to consider different number of arguments (eg. CMYK)
          if(vals.length === 4){
            mappedVals = Color.mapColorRange(vals, this._cachedMode, colorMaxes, clamp);
          }else if(vals.length === 3){
            mappedVals = Color.mapColorRange(
              [vals[0], vals[1], vals[2]],
              this._cachedMode,
              colorMaxes,
              clamp
            );
            mappedVals.push(1);
          }else if(vals.length === 2){
            // Grayscale with alpha
            if(Color.#grayscaleMap[this._cachedMode]){
              mappedVals = Color.#grayscaleMap[this._cachedMode](
                vals[0],
                colorMaxes,
                clamp
              );
            }else {
              mappedVals = Color.mapColorRange(
                [vals[0], vals[0], vals[0]],
                this._cachedMode,
                colorMaxes,
                clamp
              );
            }
            const alphaMaxes = Array.isArray(colorMaxes[colorMaxes.length-1]) ?
              colorMaxes[colorMaxes.length-1] :
              [0, colorMaxes[colorMaxes.length-1]];
            mappedVals.push(
              map(
                vals[1],
                alphaMaxes[0],
                alphaMaxes[1],
                0,
                1,
                clamp
              )
            );
          }else if(vals.length === 1){
            // Grayscale only
            if(Color.#grayscaleMap[this._cachedMode]){
              mappedVals = Color.#grayscaleMap[this._cachedMode](
                vals[0],
                colorMaxes,
                clamp
              );
            }else {
              mappedVals = Color.mapColorRange(
                [vals[0], vals[0], vals[0]],
                this._cachedMode,
                colorMaxes,
                clamp
              );
            }
            mappedVals.push(1);
          }else {
            throw new Error('Invalid color');
          }
        }else {
          mappedVals = vals;
        }
        if (this._cachedMode === RGB) {
          if (mappedVals[3] === 1) {
            // Faster for the browser to parse than rgba
            this._defaultStringValue = '#' + toHexComponent(mappedVals[0]) + toHexComponent(mappedVals[1]) + toHexComponent(mappedVals[2]);
          } else {
            this._defaultStringValue = '#' + toHexComponent(mappedVals[0]) + toHexComponent(mappedVals[1]) + toHexComponent(mappedVals[2]) + toHexComponent(mappedVals[3]);        }
        }

        this._initialize = () => {
          const space = Color.colorMap[this._cachedMode] || console.error('Invalid color mode');
          const coords = mappedVals.slice(0, 3);

          const color = {
            space,
            coords,
            alpha: mappedVals[3]
          };
          this._cachedColor = to(color, space);
        };
      }
    }

    // Color mode of the Color object, uses p5 color modes
    get mode() {
      if (this._initialize) {
        this._initialize();
        this._initialize = undefined;
      }
      return this._cachedMode;
    }
    // Reference to underlying color object depending on implementation
    // Not meant to be used publicly unless the implementation is known for sure
    get _color() {
      if (this._initialize) {
        this._initialize();
        this._initialize = undefined;
      }
      return this._cachedColor;
    }
    set _color(newColor) {
      if (this._initialize) {
        this._initialize();
        this._initialize = undefined;
      }
      this._cachedColor = newColor;
    }

    // Convert from p5 color range to color.js color range
    static mapColorRange(origin, mode, maxes, clamp){
      const p5Maxes = maxes.map(max => {
        if(!Array.isArray(max)){
          return [0, max];
        }else {
          return max;
        }
      });
      const colorjsMaxes = Color.#colorjsMaxes[mode];

      return origin.map((channel, i) => {
        const newval = map(
          channel,
          p5Maxes[i][0], p5Maxes[i][1],
          colorjsMaxes[i][0], colorjsMaxes[i][1],
          clamp
        );
        return newval;
      });
    }

    // Convert from color.js color range to p5 color range
    static unmapColorRange(origin, mode, maxes){
      const p5Maxes = maxes.map(max => {
        if(!Array.isArray(max)){
          return [0, max];
        }else {
          return max;
        }
      });
      const colorjsMaxes = Color.#colorjsMaxes[mode];

      return origin.map((channel, i) => {
        const newval = map(
          channel,
          colorjsMaxes[i][0], colorjsMaxes[i][1],
          p5Maxes[i][0], p5Maxes[i][1]
        );
        return newval;
      });
    }

    // Will do conversion in-Gamut as out of Gamut conversion is only really useful for futher conversions
    #toColorMode(mode){
      return new Color(this._color, mode);
    }

    // Get raw coordinates of underlying library, can differ between libraries
    get _array() {
      return this._getRGBA();
    }

    array(){
      return this._array;
    }

    lerp(color, amt, mode){
      // Find the closest common ancestor color space
      let spaceIndex = -1;
      while(
        (
          spaceIndex+1 < this._color.space.path.length ||
          spaceIndex+1 < color._color.space.path.length
        ) &&
        this._color.space.path[spaceIndex+1] ===
          color._color.space.path[spaceIndex+1]
      ){
        spaceIndex += 1;
      }

      if (spaceIndex === -1) {
        // This probably will not occur in practice
        throw new Error('Cannot lerp colors. No common color space found');
      }

      const obj = range(this._color, color._color, {
        space: this._color.space.path[spaceIndex].id
      })(amt);

      return new Color(obj, mode || this.mode);
    }

    /**
     * Returns the color formatted as a `String`.
     *
     * Calling `myColor.toString()` can be useful for debugging, as in
     * `print(myColor.toString())`. It's also helpful for using p5.js with other
     * libraries.
     *
     * The parameter, `format`, is optional. If a format string is passed, as in
     * `myColor.toString('#rrggbb')`, it will determine how the color string is
     * formatted. By default, color strings are formatted as `'rgba(r, g, b, a)'`.
     *
     * @param {String} [format] how the color string will be formatted.
     * Leaving this empty formats the string as rgba(r, g, b, a).
     * '#rgb' '#rgba' '#rrggbb' and '#rrggbbaa' format as hexadecimal color codes.
     * 'rgb' 'hsb' and 'hsl' return the color formatted in the specified color mode.
     * 'rgba' 'hsba' and 'hsla' are the same as above but with alpha channels.
     * 'rgb%' 'hsb%' 'hsl%' 'rgba%' 'hsba%' and 'hsla%' format as percentages.
     * @return {String} the formatted string.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object.
     *   let myColor = color('darkorchid');
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the text.
     *   text(myColor.toString('#rrggbb'), 50, 50);
     *
     *   describe('The text "#9932cc" written in purple on a gray background.');
     * }
     */
   toString(format) {
     if (format === undefined && this._defaultStringValue !== undefined) {
        return this._defaultStringValue;
     }
     
      let outputFormat = format;
      if (format === '#rrggbb') {
        outputFormat = 'hex';
      }

      const key = `${this._color.space.id}-${this._color.coords.join(',')}-${this._color.alpha}-${format}`;
      let colorString = serializationMap.get(key);

      if (!colorString) {
        colorString = serialize(this._color, {
          format: outputFormat
        });
        
        if (format === '#rrggbb') {
          colorString = String(colorString);
          if (colorString.length === 4) { 
              const r = colorString[1];
              const g = colorString[2];
              const b = colorString[3];
              colorString = `#${r}${r}${g}${g}${b}${b}`;
          }
          if (colorString.length > 7) {
              colorString = colorString.slice(0, 7);
          }
          colorString = colorString.toLowerCase();
        }

        if (serializationMap.size > 1000) {
          serializationMap.delete(serializationMap.keys().next().value);
        }
        serializationMap.set(key, colorString);
      }
      return colorString;
    }
    /**
     * Checks the contrast between two colors. This method returns a boolean
     * value to indicate if the two color has enough contrast. `true` means that
     * the colors has enough contrast to be used as background color and body
     * text color. `false` means there is not enough contrast.
     *
     * A second argument can be passed to the method, `options` , which defines
     * the algorithm to be used. The algorithms currently supported are
     * WCAG 2.1 (`'WCAG21'`) or APCA (`'APCA'`). The default is WCAG 2.1. If a
     * value of `'all'` is passed to the `options` argument, an object containing
     * more details is returned. The details object will include the calculated
     * contrast value of the colors and different passing criteria.
     *
     * For more details about color contrast, you can check out
     * <a href="https://colorjs.io/docs/contrast">this page from color.js</a>, and the
     * <a href="https://webaim.org/resources/contrastchecker/">WebAIM color contrast checker.</a>
     *
     * @param {Color} other
     * @returns {boolean|object}
     * @example
     * let bgColor, fg1Color, fg2Color, msg1, msg2;
     * function setup() {
     *   createCanvas(100, 100);
     *   bgColor = color(0);
     *   fg1Color = color(100);
     *   fg2Color = color(220);
     *
     *   if(bgColor.contrast(fg1Color)){
     *     msg1 = 'good';
     *   }else{
     *     msg1 = 'bad';
     *   }
     *
     *   if(bgColor.contrast(fg2Color)){
     *     msg2 = 'good';
     *   }else{
     *     msg2 = 'bad';
     *   }
     *
     *   describe('A black canvas with a faint grey word saying "bad" at the top left and a brighter light grey word saying "good" in the middle of the canvas.');
     * }
     *
     * function draw(){
     *   background(bgColor);
     *
     *   textSize(18);
     *
     *   fill(fg1Color);
     *   text(msg1, 10, 30);
     *
     *   fill(fg2Color);
     *   text(msg2, 10, 60);
     * }
     *
     * @example
     * let bgColor, fgColor, contrast;
     * function setup() {
     *   createCanvas(100, 100);
     *   bgColor = color(0);
     *   fgColor = color(200);
     *   contrast = bgColor.contrast(fgColor, 'all');
     *
     *   describe('A black canvas with four short lines of grey text that respectively says: "WCAG 2.1", "12.55", "APCA", and "-73.30".');
     * }
     *
     * function draw(){
     *   background(bgColor);
     *
     *   textSize(14);
     *
     *   fill(fgColor);
     *   text('WCAG 2.1', 10, 25);
     *   text(nf(contrast.WCAG21.value, 0, 2), 10, 40);
     *
     *   text('APCA', 10, 70);
     *   text(nf(contrast.APCA.value, 0, 2), 10, 85);
     * }
     */
    contrast(other_color, options='WCAG21') {
      if(options !== 'all'){
        let contrastVal, minimum;
        switch(options){
          case 'WCAG21':
            contrastVal = contrastWCAG21(this._color, other_color._color);
            minimum = 4.5;
            break;
          case 'APCA':
            contrastVal = Math.abs(contrastAPCA(this._color, other_color._color));
            minimum = 75;
            break;
          default:
            return null;
        }

        return contrastVal >= minimum;
      }else {
        const wcag21Value = contrastWCAG21(this._color, other_color._color);
        const apcaValue = contrastAPCA(this._color, other_color._color);
        return {
          WCAG21: {
            value: wcag21Value,
            passedMinimum: wcag21Value >= 4.5,
            passedAAA: wcag21Value >= 7
          },
          APCA: {
            value: apcaValue,
            passedMinimum: Math.abs(apcaValue) >= 75
          }
        };
      }
    };

    /**
     * Sets the red component of a color.
     *
     * The range depends on the <a href="#/p5/colorMode">colorMode()</a>. In the
     * default RGB mode it's between 0 and 255.
     *
     * @param {Number} red the new red value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object.
     *   let c = color(255, 128, 128);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 20, 35, 60);
     *
     *   // Change the red value.
     *   c.setRed(64);
     *
     *   // Draw the right rectangle.
     *   fill(c);
     *   rect(50, 20, 35, 60);
     *
     *   describe('Two rectangles. The left one is salmon pink and the right one is teal.');
     * }
     */
    setRed(new_red, max=[0, 1]) {
      this._defaultStringValue = undefined;
      if(!Array.isArray(max)){
        max = [0, max];
      }

      const colorjsMax = Color.#colorjsMaxes[RGB][0];
      const newval = map(new_red, max[0], max[1], colorjsMax[0], colorjsMax[1]);

      if(this.mode === RGB || this.mode === RGBHDR){
        this._color.coords[0] = newval;
      }else {
        // Will do an imprecise conversion to 'srgb', not recommended
        const space = this._color.space.id;
        const representation = to(this._color, 'srgb');
        representation.coords[0] = newval;
        this._color = to(representation, space);
      }
    }

    /**
     * Sets the green component of a color.
     *
     * The range depends on the <a href="#/p5/colorMode">colorMode()</a>. In the
     * default RGB mode it's between 0 and 255.
     *
     * @param {Number} green the new green value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object.
     *   let c = color(255, 128, 128);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 20, 35, 60);
     *
     *   // Change the green value.
     *   c.setGreen(255);
     *
     *   // Draw the right rectangle.
     *   fill(c);
     *   rect(50, 20, 35, 60);
     *
     *   describe('Two rectangles. The left one is salmon pink and the right one is yellow.');
     * }
     */
    setGreen(new_green, max=[0, 1]) {
      this._defaultStringValue = undefined;
      if(!Array.isArray(max)){
        max = [0, max];
      }

      const colorjsMax = Color.#colorjsMaxes[RGB][1];
      const newval = map(new_green, max[0], max[1], colorjsMax[0], colorjsMax[1]);

      if(this.mode === RGB || this.mode === RGBHDR){
        this._color.coords[1] = newval;
      }else {
        // Will do an imprecise conversion to 'srgb', not recommended
        const space = this._color.space.id;
        const representation = to(this._color, 'srgb');
        representation.coords[1] = newval;
        this._color = to(representation, space);
      }
    }

    /**
     * Sets the blue component of a color.
     *
     * The range depends on the <a href="#/p5/colorMode">colorMode()</a>. In the
     * default RGB mode it's between 0 and 255.
     *
     * @param {Number} blue the new blue value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object.
     *   let c = color(255, 128, 128);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 20, 35, 60);
     *
     *   // Change the blue value.
     *   c.setBlue(255);
     *
     *   // Draw the right rectangle.
     *   fill(c);
     *   rect(50, 20, 35, 60);
     *
     *   describe('Two rectangles. The left one is salmon pink and the right one is pale fuchsia.');
     * }
     */
    setBlue(new_blue, max=[0, 1]) {
      this._defaultStringValue = undefined;
      if(!Array.isArray(max)){
        max = [0, max];
      }

      const colorjsMax = Color.#colorjsMaxes[RGB][2];
      const newval = map(new_blue, max[0], max[1], colorjsMax[0], colorjsMax[1]);

      if(this.mode === RGB || this.mode === RGBHDR){
        this._color.coords[2] = newval;
      }else {
        // Will do an imprecise conversion to 'srgb', not recommended
        const space = this._color.space.id;
        const representation = to(this._color, 'srgb');
        representation.coords[2] = newval;
        this._color = to(representation, space);
      }
    }

    /**
     * Sets the alpha (transparency) value of a color.
     *
     * The range depends on the
     * <a href="#/p5/colorMode">colorMode()</a>. In the default RGB mode it's
     * between 0 and 255.
     *
     * @param {Number} alpha the new alpha value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object.
     *   let c = color(255, 128, 128);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 20, 35, 60);
     *
     *   // Change the alpha value.
     *   c.setAlpha(128);
     *
     *   // Draw the right rectangle.
     *   fill(c);
     *   rect(50, 20, 35, 60);
     *
     *   describe('Two rectangles. The left one is salmon pink and the right one is faded pink.');
     * }
     */
    setAlpha(new_alpha, max=[0, 1]) {
      this._defaultStringValue = undefined;
      if(!Array.isArray(max)){
        max = [0, max];
      }

      const colorjsMax = Color.#colorjsMaxes[this.mode][3];
      const newval = map(new_alpha, max[0], max[1], colorjsMax[0], colorjsMax[1]);

      this._color.alpha = newval;
    }

    _getRGBA(maxes=[1, 1, 1, 1]) {
      // Get colorjs maxes
      const colorjsMaxes = Color.#colorjsMaxes[RGB];

      // Normalize everything to 0,1 or the provided range (map)
      let coords = structuredClone(to(this._color, 'srgb').coords);
      coords.push(this._color.alpha);

      const rangeMaxes = maxes.map((v) => {
        if(!Array.isArray(v)){
          return [0, v];
        }else {
          return v
        }
      });

      coords = coords.map((coord, i) => {
        return map(
          coord,
          colorjsMaxes[i][0], colorjsMaxes[i][1],
          rangeMaxes[i][0], rangeMaxes[i][1]
        );
      });

      return coords;
    }

    _getMode() {
      return this.mode;
    }

    _getRed(max=[0, 1]) {
      if(!Array.isArray(max)){
        max = [0, max];
      }

      if(this.mode === RGB || this.mode === RGBHDR){
        const colorjsMax = Color.#colorjsMaxes[this.mode][0];
        return map(
          this._color.coords[0],
          colorjsMax[0], colorjsMax[1],
          max[0], max[1]
        );
      }else {
        // Will do an imprecise conversion to 'srgb', not recommended
        const colorjsMax = Color.#colorjsMaxes[RGB][0];
        return map(to(this._color, 'srgb').coords[0], colorjsMax[0], colorjsMax[1], max[0], max[1]);
      }
    }

    /**
     * This function extracts the green value from a color object and
     * returns it in the range 0–255 by default. When `colorMode()` is given to an
     * RBG value, the green value within the givin range is returned
     */
    _getGreen(max=[0, 1]) {
      if(!Array.isArray(max)){
        max = [0, max];
      }

      if(this.mode === RGB || this.mode === RGBHDR){
        const colorjsMax = Color.#colorjsMaxes[this.mode][1];
        return map(
          this._color.coords[1],
          colorjsMax[0], colorjsMax[1],
          max[0], max[1]
        );
      }else {
        // Will do an imprecise conversion to 'srgb', not recommended
        const colorjsMax = Color.#colorjsMaxes[RGB][1];
        return map(to(this._color, 'srgb').coords[1], colorjsMax[0], colorjsMax[1], max[0], max[1]);
      }
    }

    _getBlue(max=[0, 1]) {
      if(!Array.isArray(max)){
        max = [0, max];
      }

      if(this.mode === RGB || this.mode === RGBHDR){
        const colorjsMax = Color.#colorjsMaxes[this.mode][2];
        return map(
          this._color.coords[2],
          colorjsMax[0], colorjsMax[1],
          max[0], max[1]
        );
      }else {
        // Will do an imprecise conversion to 'srgb', not recommended
        const colorjsMax = Color.#colorjsMaxes[RGB][2];
        return map(to(this._color, 'srgb').coords[2], colorjsMax[0], colorjsMax[1], max[0], max[1]);
      }
    }

    _getAlpha(max=[0, 1]) {
      if(!Array.isArray(max)){
        max = [0, max];
      }

      const colorjsMax = Color.#colorjsMaxes[this.mode][3];
      return map(this._color.alpha, colorjsMax[0], colorjsMax[1], max[0], max[1]);
    }

    /**
     * Hue is the same in HSB and HSL, but the maximum value may be different.
     * This function will return the HSB-normalized saturation when supplied with
     * an HSB color object, but will default to the HSL-normalized saturation
     * otherwise.
     */
    _getHue(max=[0, 360]) {
      if(!Array.isArray(max)){
        max = [0, max];
      }

      if(this.mode === HSB || this.mode === HSL){
        const colorjsMax = Color.#colorjsMaxes[this.mode][0];
        return map(
          this._color.coords[0],
          colorjsMax[0], colorjsMax[1],
          max[0], max[1]
        );
      }else {
        // Will do an imprecise conversion to 'HSL', not recommended
        const colorjsMax = Color.#colorjsMaxes[HSL][0];
        return map(to(this._color, 'hsl').coords[0], colorjsMax[0], colorjsMax[1], max[0], max[1]);
      }
    }

    /**
     * Saturation is scaled differently in HSB and HSL. This function will return
     * the HSB saturation when supplied with an HSB color object, but will default
     * to the HSL saturation otherwise.
     */
    _getSaturation(max=[0, 100]) {
      if(!Array.isArray(max)){
        max = [0, max];
      }

      if(this.mode === HSB || this.mode === HSL){
        const colorjsMax = Color.#colorjsMaxes[this.mode][1];
        return map(
          this._color.coords[1],
          colorjsMax[0], colorjsMax[1],
          max[0], max[1]
        );
      }else {
        // Will do an imprecise conversion to 'HSL', not recommended
        const colorjsMax = Color.#colorjsMaxes[HSL][1];
        return map(to(this._color, 'hsl').coords[1], colorjsMax[0], colorjsMax[1], max[0], max[1]);
      }
    }

    /**
     * Brightness obtains the HSB brightness value from either a p5.Color object,
     * an array of color components, or a CSS color string.Depending on value,
     * when `colorMode()` is set to HSB, this function will return the
     * brightness value in the range. By default, this function will return
     * the HSB brightness within the range 0 - 100.
     */
    _getBrightness(max=[0, 100]) {
      if(!Array.isArray(max)){
        max = [0, max];
      }

      if(this.mode === HSB){
        const colorjsMax = Color.#colorjsMaxes[this.mode][2];
        return map(
          this._color.coords[2],
          colorjsMax[0], colorjsMax[1],
          max[0], max[1]
        );
      }else {
        // Will do an imprecise conversion to 'HSB', not recommended
        const colorjsMax = Color.#colorjsMaxes[HSB][2];
        return map(to(this._color, 'hsb').coords[2], colorjsMax[0], colorjsMax[1], max[0], max[1]);
      }
    }

    _getLightness(max=[0, 100]) {
      if(!Array.isArray(max)){
        max = [0, max];
      }

      if(this.mode === HSL){
        const colorjsMax = Color.#colorjsMaxes[this.mode][2];
        return map(
          this._color.coords[2],
          colorjsMax[0], colorjsMax[1],
          max[0], max[1]
        );
      }else {
        // Will do an imprecise conversion to 'HSL', not recommended
        const colorjsMax = Color.#colorjsMaxes[HSL][2];
        return map(to(this._color, 'hsl').coords[2], colorjsMax[0], colorjsMax[1], max[0], max[1]);
      }
    }
  }

  function color$1(p5, fn, lifecycles){
    /**
     * A class to describe a color.
     *
     * Each `p5.Color` object stores the color mode
     * and level maxes that were active during its construction. These values are
     * used to interpret the arguments passed to the object's constructor. They
     * also determine output formatting such as when
     * <a href="#/p5/saturation">saturation()</a> is called.
     *
     * Color is stored internally as an array of ideal RGBA values in floating
     * point form, normalized from 0 to 1. These values are used to calculate the
     * closest screen colors, which are RGBA levels from 0 to 255. Screen colors
     * are sent to the renderer.
     *
     * When different color representations are calculated, the results are cached
     * for performance. These values are normalized, floating-point numbers.
     *
     * Note: <a href="#/p5/color">color()</a> is the recommended way to create an
     * instance of this class.
     *
     * @class p5.Color
     * @param {p5} pInst                      pointer to p5 instance.
     *
     * @param {Number[]|String} vals            an array containing the color values
     *                                          for red, green, blue and alpha channel
     *                                          or CSS color.
     */
    /**
     * @class p5.Color
     * @param {Number[]|String} vals
     */
    p5.Color = Color;

    sRGB.fromGray = P3.fromGray = function(val, maxes, clamp){
      // Use blue max
      const p5Maxes = maxes.map(max => {
        if(!Array.isArray(max)){
          return [0, max];
        }else {
          return max;
        }
      });

      const v = map(val, p5Maxes[2][0], p5Maxes[2][1], 0, 1, clamp);
      return [v, v, v];
    };

    HSBSpace.fromGray = HSLSpace.fromGray = function(val, maxes, clamp){
      // Use brightness max
      const p5Maxes = maxes.map(max => {
        if(!Array.isArray(max)){
          return [0, max];
        }else {
          return max;
        }
      });

      const v = map(val, p5Maxes[2][0], p5Maxes[2][1], 0, 100, clamp);
      return [0, 0, v];
    };

    HWBSpace.fromGray = function(val, maxes, clamp){
      // Use Whiteness and Blackness to create number line
      const p5Maxes = maxes.map(max => {
        if(!Array.isArray(max)){
          return [0, max];
        }else {
          return max;
        }
      });

      const wbMax =
        (Math.abs(p5Maxes[1][0] - p5Maxes[1][1])) / 2 +
        (Math.abs(p5Maxes[2][0] - p5Maxes[2][1])) / 2;

      const nVal = map(val, 0, wbMax, 0, 100);
      let white, black;
      if(nVal < 50){
        black = nVal;
        white = 100 - nVal;
      }else if(nVal >= 50){
        white = nVal;
        black = 100 - nVal;
      }
      return [0, white, black];
    };

    Lab.fromGray =
    LCHSpace.fromGray =
    OKLab.fromGray =
    OKLCHSpace.fromGray =
    function(val, maxes, clamp){
      // Use lightness max
      const p5Maxes = maxes.map(max => {
        if(!Array.isArray(max)){
          return [0, max];
        }else {
          return max;
        }
      });

      const v = map(val, p5Maxes[0][0], p5Maxes[0][1], 0, 100, clamp);
      return [v, 0, 0];
    };

    // Register color modes and initialize Color maxes to what p5 has set for itself
    p5.Color.addColorMode(RGB, sRGB);
    p5.Color.addColorMode(RGBHDR, P3);
    p5.Color.addColorMode(HSB, HSBSpace);
    p5.Color.addColorMode(HSL, HSLSpace);
    p5.Color.addColorMode(HWB, HWBSpace);
    p5.Color.addColorMode(LAB, Lab);
    p5.Color.addColorMode(LCH, LCHSpace);
    p5.Color.addColorMode(OKLAB, OKLab);
    p5.Color.addColorMode(OKLCH, OKLCHSpace);

    lifecycles.presetup = function(){
      const pInst = this;

      // Decorate set methods
      const setMethods = ['Red', 'Green', 'Blue', 'Alpha'];
      for(let i in setMethods){
        const method = setMethods[i];
        const setCopy = p5.Color.prototype['set' + method];
        p5.Color.prototype['set' + method] = function(newval, max){
          max = max || pInst?._renderer?.states?.colorMaxes?.[RGB][i];
          return setCopy.call(this, newval, max);
        };
      }

      // Decorate get methods
      function decorateGet(channel, modes){
        const getCopy = p5.Color.prototype['_get' + channel];
        p5.Color.prototype['_get' + channel] = function(max){
          if(Object.keys(modes).includes(this.mode)){
            max = max ||
              pInst?._renderer?.states?.colorMaxes?.[this.mode][modes[this.mode]];
          }else {
            const defaultMode = Object.keys(modes)[0];
            max = max ||
              pInst
                ?._renderer
                ?.states
                ?.colorMaxes
                ?.[defaultMode][modes[defaultMode]];
          }

          return getCopy.call(this, max);
        };
      }

      decorateGet('Red', {
        [RGB]: 0,
        [RGBHDR]: 0
      });
      decorateGet('Green', {
        [RGB]: 1,
        [RGBHDR]: 1
      });
      decorateGet('Blue', {
        [RGB]: 2,
        [RGBHDR]: 2
      });
      decorateGet('Alpha', {
        [RGB]: 3,
        [RGBHDR]: 3,
        [HSB]: 3,
        [HSL]: 3,
        [HWB]: 3,
        [LAB]: 3,
        [LCH]: 3,
        [OKLAB]: 3,
        [OKLCH]: 3
      });

      decorateGet('Hue', {
        [HSL]: 0,
        [HSB]: 0,
        [HWB]: 0,
        [LCH]: 2,
        [OKLCH]: 2
      });
      decorateGet('Saturation', {
        [HSL]: 1,
        [HSB]: 1
      });
      decorateGet('Brightness', {
        [HSB]: 2
      });
      decorateGet('Lightness', {
        [HSL]: 2
      });
    };
  }

  if(typeof p5 !== 'undefined'){
    color$1(p5);
  }

  /**
   * @module Color
   * @submodule Creating & Reading
   * @for p5
   * @requires core
   * @requires constants
   */


  /**
   * @typedef {'rgb'} RGB
   * @property {RGB} RGB
   * @final
   */
  const RGB = 'rgb';
  /**
   * @typedef {'rgbhdr'} RGBHDR
   * @property {RGBHDR} RGBHDR
   * @final
   */
  const RGBHDR = 'rgbhdr';
  /**
   * HSB (hue, saturation, brightness) is a type of color model.
   * You can learn more about it at
   * <a href="https://learnui.design/blog/the-hsb-color-system-practicioners-primer.html">HSB</a>.
   *
   * @typedef {'hsb'} HSB
   * @property {HSB} HSB
   * @final
   */
  const HSB = 'hsb';
  /**
   * @typedef {'hsl'} HSL
   * @property {HSL} HSL
   * @final
   */
  const HSL = 'hsl';
  /**
   * @typedef {'hwb'} HWB
   * @property {HWB} HWB
   * @final
   */
  const HWB = 'hwb';
  /**
   * @typedef {'lab'} LAB
   * @property {LAB} LAB
   * @final
   */
  const LAB = 'lab';
  /**
   * @typedef {'lch'} LCH
   * @property {LCH} LCH
   * @final
   */
  const LCH = 'lch';
  /**
   * @typedef {'oklab'} OKLAB
   * @property {OKLAB} OKLAB
   * @final
   */
  const OKLAB = 'oklab';
  /**
   * @typedef {'oklch'} OKLCH
   * @property {OKLCH} OKLCH
   * @final
   */
  const OKLCH = 'oklch';
  /**
   * @typedef {'rgba'} RGBA
   * @property {RGBA} RGBA
   * @final
   */
  const RGBA = 'rgba';

  function creatingReading(p5, fn){
    fn.RGB = RGB;
    fn.RGBHDR = RGBHDR;
    fn.HSB = HSB;
    fn.HSL = HSL;
    fn.HWB = HWB;

    fn.LAB = LAB;
    fn.LCH = LCH;

    fn.OKLAB = OKLAB;
    fn.OKLCH = OKLCH;

    fn.RGBA = RGBA;

    // Add color states to renderer state machine
    p5.Renderer.states.colorMode = RGB;
    p5.Renderer.states.colorMaxes = {
      [RGB]: [255, 255, 255, 255],
      [RGBHDR]: [255, 255, 255, 255],
      [HSB]: [360, 100, 100, 1],
      [HSL]: [360, 100, 100, 1],
      [HWB]: [360, 100, 100, 1],

      [LAB]: [100, [-125, 125], [-125, 125], 1],
      [LCH]: [100, 150, 360, 1],

      [OKLAB]: [100, [-125, 125], [-125, 125], 1],
      [OKLCH]: [100, 150, 360, 1],
      clone: function(){
        const cloned = { ...this };
        for (const key in cloned) {
          if (cloned[key] instanceof Array) {
            cloned[key] = [...cloned[key]];
          }
        }
        return cloned;
      }
    };

    /**
     * Creates a <a href="#/p5/p5.Color">p5.Color</a> object.
     *
     * By default, the parameters are interpreted as RGB values. Calling
     * `color(255, 204, 0)` will return a bright yellow color. The way these
     * parameters are interpreted may be changed with the
     * <a href="#/p5/colorMode">colorMode()</a> function.
     *
     * The version of `color()` with one parameter interprets the value one of two
     * ways. If the parameter is a number, it's interpreted as a grayscale value.
     * If the parameter is a string, it's interpreted as a CSS color string.
     *
     * The version of `color()` with two parameters interprets the first one as a
     * grayscale value. The second parameter sets the alpha (transparency) value.
     *
     * The version of `color()` with three parameters interprets them as RGB, HSB,
     * or HSL colors, depending on the current `colorMode()`.
     *
     * The version of `color()` with four parameters interprets them as RGBA, HSBA,
     * or HSLA colors, depending on the current `colorMode()`. The last parameter
     * sets the alpha (transparency) value.
     *
     * @method color
     * @param  {Number} gray number specifying value between white and black.
     * @param  {Number} [alpha] alpha value relative to current color range
     *                                 (default is 0-255).
     * @return {p5.Color} resulting color.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object using RGB values.
     *   let c = color(255, 204, 0);
     *
     *   // Draw the square.
     *   fill(c);
     *   noStroke();
     *   square(30, 20, 55);
     *
     *   describe('A yellow square on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object using RGB values.
     *   let c1 = color(255, 204, 0);
     *
     *   // Draw the left circle.
     *   fill(c1);
     *   noStroke();
     *   circle(25, 25, 80);
     *
     *   // Create a p5.Color object using a grayscale value.
     *   let c2 = color(65);
     *
     *   // Draw the right circle.
     *   fill(c2);
     *   circle(75, 75, 80);
     *
     *   describe(
     *     'Two circles on a gray canvas. The circle in the top-left corner is yellow and the one at the bottom-right is gray.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object using a named color.
     *   let c = color('magenta');
     *
     *   // Draw the square.
     *   fill(c);
     *   noStroke();
     *   square(20, 20, 60);
     *
     *   describe('A magenta square on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object using a hex color code.
     *   let c1 = color('#0f0');
     *
     *   // Draw the left rectangle.
     *   fill(c1);
     *   noStroke();
     *   rect(0, 10, 45, 80);
     *
     *   // Create a p5.Color object using a hex color code.
     *   let c2 = color('#00ff00');
     *
     *   // Draw the right rectangle.
     *   fill(c2);
     *   rect(55, 10, 45, 80);
     *
     *   describe('Two bright green rectangles on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object using a RGB color string.
     *   let c1 = color('rgb(0, 0, 255)');
     *
     *   // Draw the top-left square.
     *   fill(c1);
     *   square(10, 10, 35);
     *
     *   // Create a p5.Color object using a RGB color string.
     *   let c2 = color('rgb(0%, 0%, 100%)');
     *
     *   // Draw the top-right square.
     *   fill(c2);
     *   square(55, 10, 35);
     *
     *   // Create a p5.Color object using a RGBA color string.
     *   let c3 = color('rgba(0, 0, 255, 1)');
     *
     *   // Draw the bottom-left square.
     *   fill(c3);
     *   square(10, 55, 35);
     *
     *   // Create a p5.Color object using a RGBA color string.
     *   let c4 = color('rgba(0%, 0%, 100%, 1)');
     *
     *   // Draw the bottom-right square.
     *   fill(c4);
     *   square(55, 55, 35);
     *
     *   describe('Four blue squares in the corners of a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object using a HSL color string.
     *   let c1 = color('hsl(160, 100%, 50%)');
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c1);
     *   rect(0, 10, 45, 80);
     *
     *   // Create a p5.Color object using a HSLA color string.
     *   let c2 = color('hsla(160, 100%, 50%, 0.5)');
     *
     *   // Draw the right rectangle.
     *   fill(c2);
     *   rect(55, 10, 45, 80);
     *
     *   describe('Two sea green rectangles. A darker rectangle on the left and a brighter one on the right.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object using a HSB color string.
     *   let c1 = color('hsb(160, 100%, 50%)');
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c1);
     *   rect(0, 10, 45, 80);
     *
     *   // Create a p5.Color object using a HSBA color string.
     *   let c2 = color('hsba(160, 100%, 50%, 0.5)');
     *
     *   // Draw the right rectangle.
     *   fill(c2);
     *   rect(55, 10, 45, 80);
     *
     *   describe('Two green rectangles. A darker rectangle on the left and a brighter one on the right.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object using RGB values.
     *   let c1 = color(50, 55, 100);
     *
     *   // Draw the left rectangle.
     *   fill(c1);
     *   rect(0, 10, 45, 80);
     *
     *   // Switch the color mode to HSB.
     *   colorMode(HSB, 100);
     *
     *   // Create a p5.Color object using HSB values.
     *   let c2 = color(50, 55, 100);
     *
     *   // Draw the right rectangle.
     *   fill(c2);
     *   rect(55, 10, 45, 80);
     *
     *   describe('Two blue rectangles. A darker rectangle on the left and a brighter one on the right.');
     * }
     */
    /**
     * @method color
     * @param  {Number}        v1      red or hue value relative to
     *                                 the current color range.
     * @param  {Number}        v2      green or saturation value
     *                                 relative to the current color range.
     * @param  {Number}        v3      blue or brightness value
     *                                 relative to the current color range.
     * @param  {Number}        [alpha]
     * @return {p5.Color}
     */
    /**
     * @method color
     * @param  {String}        value   a color string.
     * @return {p5.Color}
     */
    /**
     * @method color
     * @param  {Number[]}      values  an array containing the red, green, blue,
     *                                 and alpha components of the color.
     * @return {p5.Color}
     */
    /**
     * @method color
     * @param  {p5.Color}     color
     * @return {p5.Color}
     */
    fn.color = function(...args) {
      // p5._validateParameters('color', args);
      if (args[0] instanceof Color) {
        // TODO: perhaps change color mode to match instance mode?
        return args[0]; // Do nothing if argument is already a color object.
      }

      const arg = Array.isArray(args[0]) ? args[0] : args;
      return new Color(
        arg,
        this._renderer.states.colorMode,
        this._renderer.states.colorMaxes[this._renderer.states.colorMode],
        { clamp: true }
      );
    };

    /**
     * Gets the red value of a color.
     *
     * `red()` extracts the red value from a
     * <a href="/reference/p5/p5.Color/">p5.Color</a> object, an array of color components, or
     * a CSS color string.
     *
     * By default, `red()` returns a color's red value in the range 0
     * to 255. If the <a href="/reference/p5/colorMode/">colorMode()</a> is set to RGB, it
     * returns the red value in the given range.
     *
     * @method red
     * @param {p5.Color|Number[]|String} color <a href="/reference/p5/p5.Color/">p5.Color</a> object, array of
     *                                         color components, or CSS color string.
     * @return {Number} the red value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object.
     *   let c = color(175, 100, 220);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'redValue' to 175.
     *   let redValue = red(c);
     *
     *   // Draw the right rectangle.
     *   fill(redValue, 0, 0);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is light purple and the right one is red.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a color array.
     *   let c = [175, 100, 220];
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'redValue' to 175.
     *   let redValue = red(c);
     *
     *   // Draw the right rectangle.
     *   fill(redValue, 0, 0);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is light purple and the right one is red.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a CSS color string.
     *   let c = 'rgb(175, 100, 220)';
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'redValue' to 175.
     *   let redValue = red(c);
     *
     *   // Draw the right rectangle.
     *   fill(redValue, 0, 0);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is light purple and the right one is red.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use RGB color with values in the range 0-100.
     *   colorMode(RGB, 100);
     *
     *   // Create a p5.Color object.
     *   let c = color(69, 39, 86);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'redValue' to 69.
     *   let redValue = red(c);
     *
     *   // Draw the right rectangle.
     *   fill(redValue, 0, 0);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is light purple and the right one is red.');
     * }
     */
    fn.red = function(c) {
      // p5._validateParameters('red', arguments);
      // Get current red max
      return this.color(c)._getRed();
    };

    /**
     * Gets the green value of a color.
     *
     * `green()` extracts the green value from a
     * <a href="/reference/p5/p5.Color/">p5.Color</a> object, an array of color components, or
     * a CSS color string.
     *
     * By default, `green()` returns a color's green value in the range 0
     * to 255. If the <a href="/reference/p5/colorMode/">colorMode()</a> is set to RGB, it
     * returns the green value in the given range.
     *
     * @method green
     * @param {p5.Color|Number[]|String} color <a href="/reference/p5/p5.Color/">p5.Color</a> object, array of
     *                                         color components, or CSS color string.
     * @return {Number} the green value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object.
     *   let c = color(175, 100, 220);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'greenValue' to 100.
     *   let greenValue = green(c);
     *
     *   // Draw the right rectangle.
     *   fill(0, greenValue, 0);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is light purple and the right one is dark green.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a color array.
     *   let c = [175, 100, 220];
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'greenValue' to 100.
     *   let greenValue = green(c);
     *
     *   // Draw the right rectangle.
     *   fill(0, greenValue, 0);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is light purple and the right one is dark green.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a CSS color string.
     *   let c = 'rgb(175, 100, 220)';
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'greenValue' to 100.
     *   let greenValue = green(c);
     *
     *   // Draw the right rectangle.
     *   fill(0, greenValue, 0);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is light purple and the right one is dark green.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use RGB color with values in the range 0-100.
     *   colorMode(RGB, 100);
     *
     *   // Create a p5.Color object using RGB values.
     *   let c = color(69, 39, 86);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'greenValue' to 39.
     *   let greenValue = green(c);
     *
     *   // Draw the right rectangle.
     *   fill(0, greenValue, 0);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is light purple and the right one is dark green.');
     * }
     */
    fn.green = function(c) {
      // p5._validateParameters('green', arguments);
      // Get current green max
      return this.color(c)._getGreen();
    };

    /**
     * Gets the blue value of a color.
     *
     * `blue()` extracts the blue value from a
     * <a href="/reference/p5/p5.Color/">p5.Color</a> object, an array of color components, or
     * a CSS color string.
     *
     * By default, `blue()` returns a color's blue value in the range 0
     * to 255. If the <a href="/reference/p5/colorMode/">colorMode()</a> is set to RGB, it
     * returns the blue value in the given range.
     *
     * @method blue
     * @param {p5.Color|Number[]|String} color <a href="/reference/p5/p5.Color/">p5.Color</a> object, array of
     *                                         color components, or CSS color string.
     * @return {Number} the blue value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object using RGB values.
     *   let c = color(175, 100, 220);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'blueValue' to 220.
     *   let blueValue = blue(c);
     *
     *   // Draw the right rectangle.
     *   fill(0, 0, blueValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is light purple and the right one is royal blue.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a color array.
     *   let c = [175, 100, 220];
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'blueValue' to 220.
     *   let blueValue = blue(c);
     *
     *   // Draw the right rectangle.
     *   fill(0, 0, blueValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is light purple and the right one is royal blue.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a CSS color string.
     *   let c = 'rgb(175, 100, 220)';
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'blueValue' to 220.
     *   let blueValue = blue(c);
     *
     *   // Draw the right rectangle.
     *   fill(0, 0, blueValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is light purple and the right one is royal blue.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use RGB color with values in the range 0-100.
     *   colorMode(RGB, 100);
     *
     *   // Create a p5.Color object using RGB values.
     *   let c = color(69, 39, 86);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'blueValue' to 86.
     *   let blueValue = blue(c);
     *
     *   // Draw the right rectangle.
     *   fill(0, 0, blueValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is light purple and the right one is royal blue.');
     * }
     */
    fn.blue = function(c) {
      // p5._validateParameters('blue', arguments);
      // Get current blue max
      return this.color(c)._getBlue();
    };

    /**
     * Gets the alpha (transparency) value of a color.
     *
     * `alpha()` extracts the alpha value from a
     * <a href="#/p5.Color">p5.Color</a> object, an array of color components, or
     * a CSS color string.
     *
     * @method alpha
     * @param {p5.Color|Number[]|String} color <a href="#/p5.Color">p5.Color</a> object, array of
     *                                         color components, or CSS color string.
     * @return {Number} the alpha value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object.
     *   let c = color(0, 126, 255, 102);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'alphaValue' to 102.
     *   let alphaValue = alpha(c);
     *
     *   // Draw the right rectangle.
     *   fill(alphaValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is light blue and the right one is charcoal gray.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a color array.
     *   let c = [0, 126, 255, 102];
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'alphaValue' to 102.
     *   let alphaValue = alpha(c);
     *
     *   // Draw the left rectangle.
     *   fill(alphaValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is light blue and the right one is charcoal gray.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a CSS color string.
     *   let c = 'rgba(0, 126, 255, 0.4)';
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'alphaValue' to 102.
     *   let alphaValue = alpha(c);
     *
     *   // Draw the right rectangle.
     *   fill(alphaValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is light blue and the right one is charcoal gray.');
     * }
     */
    fn.alpha = function(c) {
      // p5._validateParameters('alpha', arguments);
      // Get current alpha max
      return this.color(c)._getAlpha();
    };

    /**
     * Gets the hue value of a color.
     *
     * `hue()` extracts the hue value from a
     * <a href="/reference/p5/p5.Color/">p5.Color</a> object, an array of color components, or
     * a CSS color string.
     *
     * Hue describes a color's position on the color wheel. By default, `hue()`
     * returns a color's HSL hue in the range 0 to 360. If the
     * <a href="/reference/p5/colorMode/">colorMode()</a> is set to HSB or HSL, it returns the hue
     * value in the given mode.
     *
     * @method hue
     * @param {p5.Color|Number[]|String} color <a href="/reference/p5/p5.Color/">p5.Color</a> object, array of
     *                                         color components, or CSS color string.
     * @return {Number} the hue value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use HSL color.
     *   colorMode(HSL);
     *
     *   // Create a p5.Color object.
     *   let c = color(0, 50, 100);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 20, 35, 60);
     *
     *   // Set 'hueValue' to 0.
     *   let hueValue = hue(c);
     *
     *   // Draw the right rectangle.
     *   fill(hueValue);
     *   rect(50, 20, 35, 60);
     *
     *   describe(
     *     'Two rectangles. The rectangle on the left is salmon pink and the one on the right is black.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use HSL color.
     *   colorMode(HSL);
     *
     *   // Create a color array.
     *   let c = [0, 50, 100];
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 20, 35, 60);
     *
     *   // Set 'hueValue' to 0.
     *   let hueValue = hue(c);
     *
     *   // Draw the right rectangle.
     *   fill(hueValue);
     *   rect(50, 20, 35, 60);
     *
     *   describe(
     *     'Two rectangles. The rectangle on the left is salmon pink and the one on the right is black.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use HSL color.
     *   colorMode(HSL);
     *
     *   // Create a CSS color string.
     *   let c = 'rgb(255, 128, 128)';
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 20, 35, 60);
     *
     *   // Set 'hueValue' to 0.
     *   let hueValue = hue(c);
     *
     *   // Draw the right rectangle.
     *   fill(hueValue);
     *   rect(50, 20, 35, 60);
     *
     *   describe(
     *     'Two rectangles. The rectangle on the left is salmon pink and the one on the right is black.'
     *   );
     * }
     */
    fn.hue = function(c) {
      let colorMode = HSL;
      let i = 0;

      if(
        this._renderer.states.colorMode === HSB ||
        this._renderer.states.colorMode === HSL
      ){
        colorMode = this._renderer.states.colorMode;
      }else if(
        this._renderer.states.colorMode === LCH ||
        this._renderer.states.colorMode === OKLCH
      ){
        colorMode = this._renderer.states.colorMode;
        i = 2;
      }

      return this.color(c)._getHue(
        this._renderer.states.colorMaxes[colorMode][i]
      );
    };

    /**
     * Gets the saturation value of a color.
     *
     * `saturation()` extracts the saturation value from a
     * <a href="/reference/p5/p5.Color/">p5.Color</a> object, an array of color components, or
     * a CSS color string.
     *
     * Saturation is scaled differently in HSB and HSL. By default, `saturation()`
     * returns a color's HSL saturation in the range 0 to 100. If the
     * <a href="/reference/p5/colorMode/">colorMode()</a> is set to HSB or HSL, it returns the
     * saturation value in the given mode.
     *
     * @method saturation
     * @param {p5.Color|Number[]|String} color <a href="/reference/p5/p5.Color/">p5.Color</a> object, array of
     *                                         color components, or CSS color string.
     * @return {Number} the saturation value
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Use HSB color.
     *   colorMode(HSB);
     *
     *   // Create a p5.Color object.
     *   let c = color(0, 50, 100);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'satValue' to 50.
     *   let satValue = saturation(c);
     *
     *   // Draw the right rectangle.
     *   fill(satValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is salmon pink and the right one is dark gray.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Use HSB color.
     *   colorMode(HSB);
     *
     *   // Create a color array.
     *   let c = [0, 50, 100];
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'satValue' to 100.
     *   let satValue = saturation(c);
     *
     *   // Draw the right rectangle.
     *   fill(satValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is salmon pink and the right one is gray.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Use HSB color.
     *   colorMode(HSB);
     *
     *   // Create a CSS color string.
     *   let c = 'rgb(255, 128, 128)';
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'satValue' to 100.
     *   let satValue = saturation(c);
     *
     *   // Draw the right rectangle.
     *   fill(satValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is salmon pink and the right one is gray.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Use HSL color.
     *   colorMode(HSL);
     *
     *   // Create a p5.Color object.
     *   let c = color(0, 100, 75);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'satValue' to 100.
     *   let satValue = saturation(c);
     *
     *   // Draw the right rectangle.
     *   fill(satValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is salmon pink and the right one is white.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Use HSL color with values in the range 0-255.
     *   colorMode(HSL, 255);
     *
     *   // Create a p5.Color object.
     *   let c = color(0, 255, 191.5);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'satValue' to 255.
     *   let satValue = saturation(c);
     *
     *   // Draw the right rectangle.
     *   fill(satValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is salmon pink and the right one is white.');
     * }
     */
    fn.saturation = function(c) {
      const colorMode = (this._renderer.states.colorMode === HSB) ? HSB : HSL;
      return this.color(c)._getSaturation(
        this._renderer.states.colorMaxes[colorMode][1]
      );
    };

    /**
     * Gets the brightness value of a color.
     *
     * `brightness()` extracts the HSB brightness value from a
     * <a href="/reference/p5/p5.Color/">p5.Color</a> object, an array of color components, or
     * a CSS color string.
     *
     * By default, `brightness()` returns a color's HSB brightness in the range 0
     * to 100. If the <a href="/reference/p5/colorMode/">colorMode()</a> is set to HSB, it
     * returns the brightness value in the given range.
     *
     * @method brightness
     * @param {p5.Color|Number[]|String} color <a href="/reference/p5/p5.Color/">p5.Color</a> object, array of
     *                                         color components, or CSS color string.
     * @return {Number} the brightness value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use HSB color.
     *   colorMode(HSB);
     *
     *   // Create a p5.Color object.
     *   let c = color(0, 50, 100);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'brightValue' to 100.
     *   let brightValue = brightness(c);
     *
     *   // Draw the right rectangle.
     *   fill(brightValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is salmon pink and the right one is white.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use HSB color.
     *   colorMode(HSB);
     *
     *   // Create a color array.
     *   let c = [0, 50, 100];
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'brightValue' to 100.
     *   let brightValue = brightness(c);
     *
     *   // Draw the right rectangle.
     *   fill(brightValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is salmon pink and the right one is white.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use HSB color.
     *   colorMode(HSB);
     *
     *   // Create a CSS color string.
     *   let c = 'rgb(255, 128, 128)';
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'brightValue' to 100.
     *   let brightValue = brightness(c);
     *
     *   // Draw the right rectangle.
     *   fill(brightValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is salmon pink and the right one is white.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use HSB color with values in the range 0-255.
     *   colorMode(HSB, 255);
     *
     *   // Create a p5.Color object.
     *   let c = color(0, 127, 255);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'brightValue' to 255.
     *   let brightValue = brightness(c);
     *
     *   // Draw the right rectangle.
     *   fill(brightValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is salmon pink and the right one is white.');
     * }
     */
    fn.brightness = function(c) {
      return this.color(c)._getBrightness(
        this._renderer.states.colorMaxes.hsb[2]
      );
    };

    /**
     * Gets the lightness value of a color.
     *
     * `lightness()` extracts the HSL lightness value from a
     * <a href="/reference/p5/p5.Color/">p5.Color</a> object, an array of color components, or
     * a CSS color string.
     *
     * By default, `lightness()` returns a color's HSL lightness in the range 0
     * to 100. If the <a href="/reference/p5/colorMode/">colorMode()</a> is set to HSL, it
     * returns the lightness value in the given range.
     *
     * @method lightness
     * @param {p5.Color|Number[]|String} color <a href="/reference/p5/p5.Color/">p5.Color</a> object, array of
     *                                         color components, or CSS color string.
     * @return {Number} the lightness value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Use HSL color.
     *   colorMode(HSL);
     *
     *   // Create a p5.Color object using HSL values.
     *   let c = color(0, 100, 75);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'lightValue' to 75.
     *   let lightValue = lightness(c);
     *
     *   // Draw the right rectangle.
     *   fill(lightValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is salmon pink and the right one is gray.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Use HSL color.
     *   colorMode(HSL);
     *
     *   // Create a color array.
     *   let c = [0, 100, 75];
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'lightValue' to 75.
     *   let lightValue = lightness(c);
     *
     *   // Draw the right rectangle.
     *   fill(lightValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is salmon pink and the right one is gray.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Use HSL color.
     *   colorMode(HSL);
     *
     *   // Create a CSS color string.
     *   let c = 'rgb(255, 128, 128)';
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'lightValue' to 75.
     *   let lightValue = lightness(c);
     *
     *   // Draw the right rectangle.
     *   fill(lightValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is salmon pink and the right one is gray.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Use HSL color with values in the range 0-255.
     *   colorMode(HSL, 255);
     *
     *   // Create a p5.Color object using HSL values.
     *   let c = color(0, 255, 191.5);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(c);
     *   rect(15, 15, 35, 70);
     *
     *   // Set 'lightValue' to 191.5.
     *   let lightValue = lightness(c);
     *
     *   // Draw the right rectangle.
     *   fill(lightValue);
     *   rect(50, 15, 35, 70);
     *
     *   describe('Two rectangles. The left one is salmon pink and the right one is gray.');
     * }
     */
    fn.lightness = function(c) {
      return this.color(c)._getLightness(
        this._renderer.states.colorMaxes.hsl[2]
      );
    };

    /**
     * Blends two colors to find a third color between them.
     *
     * The `amt` parameter specifies the amount to interpolate between the two
     * values. 0 is equal to the first color, 0.1 is very near the first color,
     * 0.5 is halfway between the two colors, and so on. Negative numbers are set
     * to 0. Numbers greater than 1 are set to 1. This differs from the behavior of
     * <a href="#/lerp">lerp</a>. It's necessary because numbers outside of the
     * interval [0, 1] will produce strange and unexpected colors.
     *
     * The way that colors are interpolated depends on the current
     * <a href="#/colorMode">colorMode()</a>.
     *
     * @method lerpColor
     * @param  {p5.Color} c1  interpolate from this color.
     * @param  {p5.Color} c2  interpolate to this color.
     * @param  {Number}   amt number between 0 and 1.
     * @return {p5.Color}     interpolated color.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create p5.Color objects to interpolate between.
     *   let from = color(218, 165, 32);
     *   let to = color(72, 61, 139);
     *
     *   // Create intermediate colors.
     *   let interA = lerpColor(from, to, 0.33);
     *   let interB = lerpColor(from, to, 0.66);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(from);
     *   rect(10, 20, 20, 60);
     *
     *   // Draw the left-center rectangle.
     *   fill(interA);
     *   rect(30, 20, 20, 60);
     *
     *   // Draw the right-center rectangle.
     *   fill(interB);
     *   rect(50, 20, 20, 60);
     *
     *   // Draw the right rectangle.
     *   fill(to);
     *   rect(70, 20, 20, 60);
     *
     *   describe(
     *     'Four rectangles. From left to right, the rectangles are tan, brown, brownish purple, and purple.'
     *   );
     * }
     */
    fn.lerpColor = function(c1, c2, amt) {
      // p5._validateParameters('lerpColor', arguments);
      return c1.lerp(c2, amt, this._renderer.states.colorMode);
    };

    /**
     * Blends multiple colors to find a color between them.
     *
     * The `amt` parameter specifies the amount to interpolate between the color
     * stops which are colors at each `amt` value "location" with `amt` values
     * that are between 2 color stops interpolating between them based on its relative
     * distance to both.
     *
     * The way that colors are interpolated depends on the current
     * <a href="/reference/p5/colorMode/">colorMode()</a>.
     *
     * @method paletteLerp
     * @param {Array<[(p5.Color|String|Number|Number[]), Number]>} colors_stops color stops to interpolate from
     * @param {Number} amt number to use to interpolate relative to color stops
     * @return {p5.Color} interpolated color.
     *
     * @example
     * function setup() {
     *   createCanvas(400, 400);
     * }
     *
     * function draw() {
     *   // The background goes from white to red to green to blue fill
     *   background(paletteLerp([
     *     ['white', 0],
     *     ['red', 0.05],
     *     ['green', 0.25],
     *     ['blue', 1]
     *   ], millis() / 10000 % 1));
     * }
     */
    fn.paletteLerp = function(color_stops, amt) {
      const first_color_stop = color_stops[0];
      if (amt < first_color_stop[1])
        return this.color(first_color_stop[0]);

      for (let i = 1; i < color_stops.length; i++) {
        const color_stop = color_stops[i];
        if (amt < color_stop[1]) {
          const prev_color_stop = color_stops[i - 1];
          return this.lerpColor(
            this.color(prev_color_stop[0]),
            this.color(color_stop[0]),
            (amt - prev_color_stop[1]) / (color_stop[1] - prev_color_stop[1])
          );
        }
      }

      return this.color(color_stops[color_stops.length - 1][0]);
    };
  }

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

  /**
   * @module IO
   * @submodule Input
   * @requires core
   */

  class XML {
    constructor(DOM){
      if (!DOM) {
        const xmlDoc = document.implementation.createDocument(null, 'doc');
        this.DOM = xmlDoc.createElement('root');
      } else {
        this.DOM = DOM;
      }
    }

    /**
     * Returns the element's parent element as a new <a href="#/p5.XML">p5.XML</a>
     * object.
     *
     * @return {p5.XML} parent element.
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get an array with all mammal elements.
     *   let mammals = myXML.getChildren('mammal');
     *
     *   // Get the first mammal element.
     *   let firstMammal = mammals[0];
     *
     *   // Get the parent element.
     *   let parent = firstMammal.getParent();
     *
     *   // Get the parent element's name.
     *   let name = parent.getName();
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Display the parent element's name.
     *   text(name, 50, 50);
     *
     *   describe('The word "animals" written in black on a gray background.');
     * }
     */
    getParent() {
      return new XML(this.DOM.parentElement);
    }

    /**
     * Returns the element's name as a `String`.
     *
     * An XML element's name is given by its tag. For example, the element
     * `&lt;language&gt;JavaScript&lt;/language&gt;` has the name `language`.
     *
     * @return {String} name of the element.
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get an array with all mammal elements.
     *   let mammals = myXML.getChildren('mammal');
     *
     *   // Get the first mammal element.
     *   let firstMammal = mammals[0];
     *
     *   // Get the mammal element's name.
     *   let name = firstMammal.getName();
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Display the element's name.
     *   text(name, 50, 50);
     *
     *   describe('The word "mammal" written in black on a gray background.');
     * }
     */
    getName() {
      return this.DOM.tagName;
    }

    /**
     * Sets the element's tag name.
     *
     * An XML element's name is given by its tag. For example, the element
     * `&lt;language&gt;JavaScript&lt;/language&gt;` has the name `language`.
     *
     * The parameter, `name`, is the element's new name as a string. For example,
     * calling `myXML.setName('planet')` will make the element's new tag name
     * `&lt;planet&gt;&lt;/planet&gt;`.
     *
     * @param {String} name new tag name of the element.
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the element's original name.
     *   let oldName = myXML.getName();
     *
     *   // Set the element's name.
     *   myXML.setName('monsters');
     *
     *   // Get the element's new name.
     *   let newName = myXML.getName();
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Display the element's names.
     *   text(oldName, 50, 33);
     *   text(newName, 50, 67);
     *
     *   describe(
     *     'The words "animals" and "monsters" written on separate lines. The text is black on a gray background.'
     *   );
     * }
     */
    setName(name) {
      const content = this.DOM.innerHTML;
      const attributes = this.DOM.attributes;
      const xmlDoc = document.implementation.createDocument(null, 'default');
      const newDOM = xmlDoc.createElement(name);
      newDOM.innerHTML = content;
      for (let i = 0; i < attributes.length; i++) {
        newDOM.setAttribute(attributes[i].nodeName, attributes[i].nodeValue);
      }
      this.DOM = newDOM;
    }

    /**
     * Returns `true` if the element has child elements and `false` if not.
     *
     * @return {boolean} whether the element has children.
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Check whether the element has child elements.
     *   let isParent = myXML.hasChildren();
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Style the text.
     *   if (isParent === true) {
     *     text('Parent', 50, 50);
     *   } else {
     *     text('Not Parent', 50, 50);
     *   }
     *
     *   describe('The word "Parent" written in black on a gray background.');
     * }
     */
    hasChildren() {
      return this.DOM.children.length > 0;
    }

    /**
     * Returns an array with the names of the element's child elements as
     * `String`s.
     *
     * @return {String[]} names of the child elements.
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the names of the element's children as an array.
     *   let children = myXML.listChildren();
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Iterate over the array.
     *   for (let i = 0; i < children.length; i += 1) {
     *
     *     // Calculate the y-coordinate.
     *     let y = (i + 1) * 25;
     *
     *     // Display the child element's name.
     *     text(children[i], 10, y);
     *   }
     *
     *   describe(
     *     'The words "mammal", "mammal", "mammal", and "reptile" written on separate lines. The text is black on a gray background.'
     *   );
     * }
     */
    listChildren() {
      const arr = [];
      for (let i = 0; i < this.DOM.childNodes.length; i++) {
        arr.push(this.DOM.childNodes[i].nodeName);
      }
      return arr;
    }

    /**
     * Returns an array with the element's child elements as new
     * <a href="#/p5.XML">p5.XML</a> objects.
     *
     * The parameter, `name`, is optional. If a string is passed, as in
     * `myXML.getChildren('cat')`, then the method will only return child elements
     * with the tag `&lt;cat&gt;`.
     *
     * @param {String} [name] name of the elements to return.
     * @return {p5.XML[]} child elements.
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get an array of the child elements.
     *   let children = myXML.getChildren();
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Iterate over the array.
     *   for (let i = 0; i < children.length; i += 1) {
     *
     *     // Calculate the y-coordinate.
     *     let y = (i + 1) * 20;
     *
     *     // Get the child element's content.
     *     let content = children[i].getContent();
     *
     *     // Display the child element's content.
     *     text(content, 10, y);
     *   }
     *
     *   describe(
     *     'The words "Goat", "Leopard", "Zebra", and "Turtle" written on separate lines. The text is black on a gray background.'
     *   );
     * }
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get an array of the child elements
     *   // that are mammals.
     *   let children = myXML.getChildren('mammal');
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Iterate over the array.
     *   for (let i = 0; i < children.length; i += 1) {
     *
     *     // Calculate the y-coordinate.
     *     let y = (i + 1) * 20;
     *
     *     // Get the child element's content.
     *     let content = children[i].getContent();
     *
     *     // Display the child element's content.
     *     text(content, 10, y);
     *   }
     *
     *   describe(
     *     'The words "Goat", "Leopard", and "Zebra" written on separate lines. The text is black on a gray background.'
     *   );
     * }
     */
    getChildren(param) {
      if (param) {
        return elementsToP5XML(this.DOM.getElementsByTagName(param));
      } else {
        return elementsToP5XML(this.DOM.children);
      }
    }

    /**
     * Returns the first matching child element as a new
     * <a href="#/p5.XML">p5.XML</a> object.
     *
     * The parameter, `name`, is optional. If a string is passed, as in
     * `myXML.getChild('cat')`, then the first child element with the tag
     * `&lt;cat&gt;` will be returned. If a number is passed, as in
     * `myXML.getChild(1)`, then the child element at that index will be returned.
     *
     * @param {String|Integer} name element name or index.
     * @return {p5.XML} child element.
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the first child element that is a mammal.
     *   let goat = myXML.getChild('mammal');
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Get the child element's content.
     *   let content = goat.getContent();
     *
     *   // Display the child element's content.
     *   text(content, 50, 50);
     *
     *   describe('The word "Goat" written in black on a gray background.');
     * }
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the child element at index 1.
     *   let leopard = myXML.getChild(1);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Get the child element's content.
     *   let content = leopard.getContent();
     *
     *   // Display the child element's content.
     *   text(content, 50, 50);
     *
     *   describe('The word "Leopard" written in black on a gray background.');
     * }
     */
    getChild(param) {
      if (typeof param === 'string') {
        for (const child of this.DOM.children) {
          if (child.tagName === param) return new XML(child);
        }
      } else {
        return new XML(this.DOM.children[param]);
      }
    }

    /**
     * Adds a new child element and returns a reference to it.
     *
     * The parameter, `child`, is the <a href="#/p5.XML">p5.XML</a> object to add
     * as a child element. For example, calling `myXML.addChild(otherXML)` inserts
     * `otherXML` as a child element of `myXML`.
     *
     * @param {p5.XML} child child element to add.
     * @return {p5.XML} added child element.
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a new p5.XML object.
     *   let newAnimal = new p5.XML();
     *
     *   // Set its properties.
     *   newAnimal.setName('hydrozoa');
     *   newAnimal.setAttribute('id', 4);
     *   newAnimal.setAttribute('species', 'Physalia physalis');
     *   newAnimal.setContent('Bluebottle');
     *
     *   // Add the child element.
     *   myXML.addChild(newAnimal);
     *
     *   // Get the first child element that is a hydrozoa.
     *   let blueBottle = myXML.getChild('hydrozoa');
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Get the child element's content.
     *   let content = blueBottle.getContent();
     *
     *   // Display the child element's content.
     *   text(content, 50, 50);
     *
     *   describe('The word "Bluebottle" written in black on a gray background.');
     * }
     */
    addChild(node) {
      if (node instanceof XML) {
        this.DOM.appendChild(node.DOM);
      }
    }

    /**
     * Removes the first matching child element.
     *
     * The parameter, `name`, is the child element to remove. If a string is
     * passed, as in `myXML.removeChild('cat')`, then the first child element
     * with the tag `&lt;cat&gt;` will be removed. If a number is passed, as in
     * `myXML.removeChild(1)`, then the child element at that index will be
     * removed.
     *
     * @param {String|Integer} name name or index of the child element to remove.
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Remove the first mammal element.
     *   myXML.removeChild('mammal');
     *
     *   // Get an array of child elements.
     *   let children = myXML.getChildren();
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Iterate over the array.
     *   for (let i = 0; i < children.length; i += 1) {
     *
     *     // Calculate the y-coordinate.
     *     let y = (i + 1) * 25;
     *
     *     // Get the child element's content.
     *     let content = children[i].getContent();
     *
     *     // Display the child element's content.
     *     text(content, 10, y);
     *   }
     *
     *   describe(
     *     'The words "Leopard", "Zebra", and "Turtle" written on separate lines. The text is black on a gray background.'
     *   );
     * }
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Remove the element at index 2.
     *   myXML.removeChild(2);
     *
     *   // Get an array of child elements.
     *   let children = myXML.getChildren();
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Iterate over the array.
     *   for (let i = 0; i < children.length; i += 1) {
     *
     *     // Calculate the y-coordinate.
     *     let y = (i + 1) * 25;
     *
     *     // Get the child element's content.
     *     let content = children[i].getContent();
     *
     *     // Display the child element's content.
     *     text(content, 10, y);
     *   }
     *
     *   describe(
     *     'The words "Goat", "Leopard", and "Turtle" written on separate lines. The text is black on a gray background.'
     *   );
     * }
     */
    removeChild(param) {
      let ind = -1;
      if (typeof param === 'string') {
        for (let i = 0; i < this.DOM.children.length; i++) {
          if (this.DOM.children[i].tagName === param) {
            ind = i;
            break;
          }
        }
      } else {
        ind = param;
      }
      if (ind !== -1) {
        this.DOM.removeChild(this.DOM.children[ind]);
      }
    }

    /**
     * Returns the number of attributes the element has.
     *
     * @return {Integer} number of attributes.
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the first child element.
     *   let first = myXML.getChild(0);
     *
     *   // Get the number of attributes.
     *   let numAttributes = first.getAttributeCount();
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Display the number of attributes.
     *   text(numAttributes, 50, 50);
     *
     *   describe('The number "2" written in black on a gray background.');
     * }
     */
    getAttributeCount() {
      return this.DOM.attributes.length;
    }

    /**
     * Returns an `Array` with the names of the element's attributes.
     *
     * Note: Use
     * <a href="#/p5.XML/getString">myXML.getString()</a> or
     * <a href="#/p5.XML/getNum">myXML.getNum()</a> to return an attribute's value.
     *
     * @return {String[]} attribute names.
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the first child element.
     *   let first = myXML.getChild(0);
     *
     *   // Get the number of attributes.
     *   let attributes = first.listAttributes();
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Display the element's attributes.
     *   text(attributes, 50, 50);
     *
     *   describe('The text "id,species" written in black on a gray background.');
     * }
     */
    listAttributes() {
      const arr = [];

      for (const attribute of this.DOM.attributes) {
        arr.push(attribute.nodeName);
      }

      return arr;
    }

    /**
     * Returns `true` if the element has a given attribute and `false` if not.
     *
     * The parameter, `name`, is a string with the name of the attribute being
     * checked.
     *
     * Note: Use
     * <a href="#/p5.XML/getString">myXML.getString()</a> or
     * <a href="#/p5.XML/getNum">myXML.getNum()</a> to return an attribute's value.
     *
     * @param {String} name name of the attribute to be checked.
     * @return {boolean} whether the element has the attribute.
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the first mammal child element.
     *   let mammal = myXML.getChild('mammal');
     *
     *   // Check whether the element has an
     *   // species attribute.
     *   let hasSpecies = mammal.hasAttribute('species');
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Display whether the element has a species attribute.
     *   if (hasSpecies === true) {
     *     text('Species', 50, 50);
     *   } else {
     *     text('No species', 50, 50);
     *   }
     *
     *   describe('The text "Species" written in black on a gray background.');
     * }
     */
    hasAttribute(name) {
      const obj = {};

      for (const attribute of this.DOM.attributes) {
        obj[attribute.nodeName] = attribute.nodeValue;
      }

      return obj[name] ? true : false;
    }

    /**
     * Return an attribute's value as a `Number`.
     *
     * The first parameter, `name`, is a string with the name of the attribute
     * being checked. For example, calling `myXML.getNum('id')` returns the
     * element's `id` attribute as a number.
     *
     * The second parameter, `defaultValue`, is optional. If a number is passed,
     * as in `myXML.getNum('id', -1)`, it will be returned if the attribute
     * doesn't exist or can't be converted to a number.
     *
     * Note: Use
     * <a href="#/p5.XML/getString">myXML.getString()</a> or
     * <a href="#/p5.XML/getNum">myXML.getNum()</a> to return an attribute's value.
     *
     * @param {String} name name of the attribute to be checked.
     * @param {Number} [defaultValue] value to return if the attribute doesn't exist.
     * @return {Number} attribute value as a number.
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the first reptile child element.
     *   let reptile = myXML.getChild('reptile');
     *
     *   // Get the reptile's content.
     *   let content = reptile.getContent();
     *
     *   // Get the reptile's ID.
     *   let id = reptile.getNum('id');
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Display the ID attribute.
     *   text(`${content} is ${id + 1}th`, 5, 50, 90);
     *
     *   describe(`The text "${content} is ${id + 1}th" written in black on a gray background.`);
     * }
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the first reptile child element.
     *   let reptile = myXML.getChild('reptile');
     *
     *   // Get the reptile's content.
     *   let content = reptile.getContent();
     *
     *   // Get the reptile's size.
     *   let weight = reptile.getNum('weight', 135);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Display the ID attribute.
     *   text(`${content} is ${weight}kg`, 5, 50, 90);
     *
     *   describe(
     *     `The text "${content} is ${weight}kg" written in black on a gray background.`
     *   );
     * }
     */
    getNum(name, defaultValue) {
      const obj = {};

      for (const attribute of this.DOM.attributes) {
        obj[attribute.nodeName] = attribute.nodeValue;
      }

      return Number(obj[name]) || defaultValue || 0;
    }

    /**
     * Return an attribute's value as a string.
     *
     * The first parameter, `name`, is a string with the name of the attribute
     * being checked. For example, calling `myXML.getString('color')` returns the
     * element's `id` attribute as a string.
     *
     * The second parameter, `defaultValue`, is optional. If a string is passed,
     * as in `myXML.getString('color', 'deeppink')`, it will be returned if the
     * attribute doesn't exist.
     *
     * Note: Use
     * <a href="#/p5.XML/getString">myXML.getString()</a> or
     * <a href="#/p5.XML/getNum">myXML.getNum()</a> to return an attribute's value.
     *
     * @param {String} name name of the attribute to be checked.
     * @param {Number} [defaultValue] value to return if the attribute doesn't exist.
     * @return {String} attribute value as a string.
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the first reptile child element.
     *   let reptile = myXML.getChild('reptile');
     *
     *   // Get the reptile's content.
     *   let content = reptile.getContent();
     *
     *   // Get the reptile's species.
     *   let species = reptile.getString('species');
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Display the species attribute.
     *   text(`${content}: ${species}`, 5, 50, 90);
     *
     *   describe(`The text "${content}: ${species}" written in black on a gray background.`);
     * }
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the first reptile child element.
     *   let reptile = myXML.getChild('reptile');
     *
     *   // Get the reptile's content.
     *   let content = reptile.getContent();
     *
     *   // Get the reptile's color.
     *   let attribute = reptile.getString('color', 'green');
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *   fill(attribute);
     *
     *   // Display the element's content.
     *   text(content, 50, 50);
     *
     *   describe(`The text "${content}" written in green on a gray background.`);
     * }
     */
    getString(name, defaultValue) {
      const obj = {};

      for (const attribute of this.DOM.attributes) {
        obj[attribute.nodeName] = attribute.nodeValue;
      }

      return obj[name] ? String(obj[name]) : defaultValue || null;
    }

    /**
     * Sets an attribute to a given value.
     *
     * The first parameter, `name`, is a string with the name of the attribute
     * being set.
     *
     * The second parameter, `value`, is the attribute's new value. For example,
     * calling `myXML.setAttribute('id', 123)` sets the `id` attribute to the
     * value 123.
     *
     * @param {String} name name of the attribute to be set.
     * @param {Number|String|Boolean} value attribute's new value.
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the first reptile child element.
     *   let reptile = myXML.getChild('reptile');
     *
     *   // Set the reptile's color.
     *   reptile.setAttribute('color', 'green');
     *
     *   // Get the reptile's content.
     *   let content = reptile.getContent();
     *
     *   // Get the reptile's color.
     *   let attribute = reptile.getString('color');
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Display the element's content.
     *   text(`${content} is ${attribute}`, 5, 50, 90);
     *
     *   describe(
     *     `The text "${content} is ${attribute}" written in green on a gray background.`
     *   );
     * }
     */
    setAttribute(name, value) {
      this.DOM.setAttribute(name, value);
    }

    /**
     * Returns the element's content as a `String`.
     *
     * The parameter, `defaultValue`, is optional. If a string is passed, as in
     * `myXML.getContent('???')`, it will be returned if the element has no
     * content.
     *
     * @param {String} [defaultValue] value to return if the element has no
     *                                content.
     * @return {String} element's content as a string.
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the first reptile child element.
     *   let reptile = myXML.getChild('reptile');
     *
     *   // Get the reptile's content.
     *   let content = reptile.getContent();
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Display the element's content.
     *   text(content, 5, 50, 90);
     *
     *   describe(`The text "${content}" written in green on a gray background.`);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.XML object.
     *   let blankSpace = new p5.XML();
     *
     *   // Get the element's content and use a default value.
     *   let content = blankSpace.getContent('Your name');
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Display the element's content.
     *   text(content, 5, 50, 90);
     *
     *   describe(`The text "${content}" written in green on a gray background.`);
     * }
     */
    getContent(defaultValue) {
      let str;
      str = this.DOM.textContent;
      str = str.replace(/\s\s+/g, ',');
      return str || defaultValue || null;
    }

    /**
     * Sets the element's content.
     *
     * An element's content is the text between its tags. For example, the element
     * `&lt;language&gt;JavaScript&lt;/language&gt;` has the content `JavaScript`.
     *
     * The parameter, `content`, is a string with the element's new content.
     *
     * @method setContent
     * @param {String} content new content for the element.
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the first reptile child element.
     *   let reptile = myXML.getChild('reptile');
     *
     *   // Get the reptile's original content.
     *   let oldContent = reptile.getContent();
     *
     *   // Set the reptile's content.
     *   reptile.setContent('Loggerhead');
     *
     *   // Get the reptile's new content.
     *   let newContent = reptile.getContent();
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Display the element's old and new content.
     *   text(`${oldContent}: ${newContent}`, 5, 50, 90);
     *
     *   describe(
     *     `The text "${oldContent}: ${newContent}" written in green on a gray background.`
     *   );
     * }
     */
    setContent(content) {
      if (!this.DOM.children.length) {
        this.DOM.textContent = content;
      }
    }

    /**
     * Returns the element as a `String`.
     *
     * `myXML.serialize()` is useful for sending the element over the network or
     * saving it to a file.
     *
     * @return {String} element as a string.
     *
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(12);
     *
     *   // Display instructions.
     *   text('Double-click to save', 5, 50, 90);
     *
     *   describe('The text "Double-click to save" written in black on a gray background.');
     * }
     *
     * // Save the file when the user double-clicks.
     * function doubleClicked() {
     *   // Create a p5.PrintWriter object.
     *   // Use the file format .xml.
     *   let myWriter = createWriter('animals', 'xml');
     *
     *   // Serialize the XML data to a string.
     *   let data = myXML.serialize();
     *
     *   // Write the data to the print stream.
     *   myWriter.write(data);
     *
     *   // Save the file and close the print stream.
     *   myWriter.close();
     * }
     */
    serialize() {
      const xmlSerializer = new XMLSerializer();
      return xmlSerializer.serializeToString(this.DOM);
    }
  }

  function elementsToP5XML(elements) {
    const arr = [];
    for (let i = 0; i < elements.length; i++) {
      arr.push(new XML(elements[i]));
    }
    return arr;
  }

  function xml(p5, fn){
    /**
     * A class to describe an XML object.
     *
     * Each `p5.XML` object provides an easy way to interact with XML data.
     * Extensible Markup Language
     * (<a href="https://developer.mozilla.org/en-US/docs/Web/XML/XML_introduction" target="_blank">XML</a>)
     * is a standard format for sending data between applications. Like HTML, the
     * XML format is based on tags and attributes, as in
     * `&lt;time units="s"&gt;1234&lt;/time&gt;`.
     *
     * Note: Use <a href="#/p5/loadXML">loadXML()</a> to load external XML files.
     *
     * @class p5.XML
     * @example
     * let myXML;
     *
     * async function setup() {
     *   // Load the XML and create a p5.XML object.
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get an array with all mammal tags.
     *   let mammals = myXML.getChildren('mammal');
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Iterate over the mammals array.
     *   for (let i = 0; i < mammals.length; i += 1) {
     *
     *     // Calculate the y-coordinate.
     *     let y = (i + 1) * 25;
     *
     *     // Get the mammal's common name.
     *     let name = mammals[i].getContent();
     *
     *     // Display the mammal's name.
     *     text(name, 20, y);
     *   }
     *
     *   describe(
     *     'The words "Goat", "Leopard", and "Zebra" written on three separate lines. The text is black on a gray background.'
     *   );
     * }
     */
    p5.XML = XML;
  }

  if(typeof p5 !== 'undefined'){
    xml(p5);
  }

  /**
   * @module DOM
   * @submodule DOM
   * @for p5.Element
   */


  class File {
    constructor(file, pInst) {
      this.file = file;

      this._pInst = pInst;

      // Splitting out the file type into two components
      // This makes determining if image or text etc simpler
      const typeList = file.type.split('/');
      this.type = typeList[0];
      this.subtype = typeList[1];
      this.name = file.name;
      this.size = file.size;
      this.data = undefined;
    }


    static _createLoader(theFile, callback) {
      const reader = new FileReader();
      reader.onload = function (e) {
        const p5file = new File(theFile);
        if (p5file.file.type === 'application/json') {
          // Parse JSON and store the result in data
          p5file.data = JSON.parse(e.target.result);
        } else if (p5file.file.type === 'text/xml') {
          // Parse XML, wrap it in p5.XML and store the result in data
          const parser = new DOMParser();
          const xml = parser.parseFromString(e.target.result, 'text/xml');
          p5file.data = new XML(xml.documentElement);
        } else {
          p5file.data = e.target.result;
        }
        callback(p5file);
      };
      return reader;
    }

    static _load(f, callback) {
      // Text or data?
      // This should likely be improved
      if (/^text\//.test(f.type) || f.type === 'application/json') {
        File._createLoader(f, callback).readAsText(f);
      } else if (!/^(video|audio)\//.test(f.type)) {
        File._createLoader(f, callback).readAsDataURL(f);
      } else {
        const file = new File(f);
        file.data = URL.createObjectURL(f);
        callback(file);
      }
    }
  }

  function file(p5, fn){
    /**
     * A class to describe a file.
     *
     * `p5.File` objects are used by
     * <a href="#/p5.Element/drop">myElement.drop()</a> and
     * created by
     * <a href="#/p5/createFileInput">createFileInput</a>.
     *
     * @class p5.File
     * @param {File} file wrapped file.
     *
     * @example
     * // Use the file input to load a
     * // file and display its info.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a file input and place it beneath the canvas.
     *   // Call displayInfo() when the file loads.
     *   let input = createFileInput(displayInfo);
     *   input.position(0, 100);
     *
     *   describe('A gray square with a file input beneath it. If the user loads a file, its info is written in black.');
     * }
     *
     * // Display the p5.File's info once it loads.
     * function displayInfo(file) {
     *   background(200);
     *
     *   // Display the p5.File's name.
     *   text(file.name, 10, 10, 80, 40);
     *
     *   // Display the p5.File's type and subtype.
     *   text(`${file.type}/${file.subtype}`, 10, 70);
     *
     *   // Display the p5.File's size in bytes.
     *   text(file.size, 10, 90);
     * }
     *
     * @example
     * // Use the file input to select an image to
     * // load and display.
     * let img;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a file input and place it beneath the canvas.
     *   // Call handleImage() when the file image loads.
     *   let input = createFileInput(handleImage);
     *   input.position(0, 100);
     *
     *   describe('A gray square with a file input beneath it. If the user selects an image file to load, it is displayed on the square.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw the image if it's ready.
     *   if (img) {
     *     image(img, 0, 0, width, height);
     *   }
     * }
     *
     * // Use the p5.File's data once it loads.
     * function handleImage(file) {
     *   // Check the p5.File's type.
     *   if (file.type === 'image') {
     *     // Create an image using using the p5.File's data.
     *     img = createImg(file.data, '');
     *
     *     // Hide the image element so it doesn't appear twice.
     *     img.hide();
     *   } else {
     *     img = null;
     *   }
     * }
     */
    p5.File = File;

    /**
     * Underlying
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/File" target="_blank">File</a>
     * object. All `File` properties and methods are accessible.
     *
     * @for p5.File
     * @property {File} file
     * @example
     * // Use the file input to load a
     * // file and display its info.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a file input and place it beneath the canvas.
     *   // Call displayInfo() when the file loads.
     *   let input = createFileInput(displayInfo);
     *   input.position(0, 100);
     *
     *   describe('A gray square with a file input beneath it. If the user loads a file, its info is written in black.');
     * }
     *
     * // Use the p5.File once it loads.
     * function displayInfo(file) {
     *   background(200);
     *
     *   // Display the p5.File's name.
     *   text(file.name, 10, 10, 80, 40);
     *
     *   // Display the p5.File's type and subtype.
     *   text(`${file.type}/${file.subtype}`, 10, 70);
     *
     *   // Display the p5.File's size in bytes.
     *   text(file.size, 10, 90);
     * }
     */

    /**
     * The file
     * <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types" target="_blank">MIME type</a>
     * as a string.
     *
     * For example, `'image'` and `'text'` are both MIME types.
     *
     * @for p5.File
     * @property {String} type
     * @example
     * // Use the file input to load a file and display its info.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a file input and place it beneath the canvas.
     *   // Call displayType() when the file loads.
     *   let input = createFileInput(displayType);
     *   input.position(0, 100);
     *
     *   describe('A gray square with a file input beneath it. If the user loads a file, its type is written in black.');
     * }
     *
     * // Display the p5.File's type once it loads.
     * function displayType(file) {
     *   background(200);
     *
     *   // Display the p5.File's type.
     *   text(`This is file's type is: ${file.type}`, 10, 10, 80, 80);
     * }
     */

    /**
     * The file subtype as a string.
     *
     * For example, a file with an `'image'`
     * <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types" target="_blank">MIME type</a>
     * may have a subtype such as ``png`` or ``jpeg``.
     *
     * @property {String} subtype
     * @for p5.File
     *
     * @example
     * // Use the file input to load a
     * // file and display its info.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a file input and place it beneath the canvas.
     *   // Call displaySubtype() when the file loads.
     *   let input = createFileInput(displaySubtype);
     *   input.position(0, 100);
     *
     *   describe('A gray square with a file input beneath it. If the user loads a file, its subtype is written in black.');
     * }
     *
     * // Display the p5.File's type once it loads.
     * function displaySubtype(file) {
     *   background(200);
     *
     *   // Display the p5.File's subtype.
     *   text(`This is file's subtype is: ${file.subtype}`, 10, 10, 80, 80);
     * }
     */

    /**
     * The file name as a string.
     *
     * @property {String} name
     * @for p5.File
     *
     * @example
     * // Use the file input to load a
     * // file and display its info.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a file input and place it beneath the canvas.
     *   // Call displayName() when the file loads.
     *   let input = createFileInput(displayName);
     *   input.position(0, 100);
     *
     *   describe('A gray square with a file input beneath it. If the user loads a file, its name is written in black.');
     * }
     *
     * // Display the p5.File's name once it loads.
     * function displayName(file) {
     *   background(200);
     *
     *   // Display the p5.File's name.
     *   text(`This is file's name is: ${file.name}`, 10, 10, 80, 80);
     * }
     */

    /**
     * The number of bytes in the file.
     *
     * @property {Number} size
     * @for p5.File
     *
     * @example
     * // Use the file input to load a file and display its info.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a file input and place it beneath the canvas.
     *   // Call displaySize() when the file loads.
     *   let input = createFileInput(displaySize);
     *   input.position(0, 100);
     *
     *   describe('A gray square with a file input beneath it. If the user loads a file, its size in bytes is written in black.');
     * }
     *
     * // Display the p5.File's size in bytes once it loads.
     * function displaySize(file) {
     *   background(200);
     *
     *   // Display the p5.File's size.
     *   text(`This is file has ${file.size} bytes.`, 10, 10, 80, 80);
     * }
     */

    /**
     * A string containing the file's data.
     *
     * Data can be either image data, text contents, or a parsed object in the
     * case of JSON and <a href="#/p5.XML">p5.XML</a> objects.
     *
     * @property {any} data
     * @for p5.File
     *
     * @example
     * // Use the file input to load a file and display its info.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a file input and place it beneath the canvas.
     *   // Call displayData() when the file loads.
     *   let input = createFileInput(displayData);
     *   input.position(0, 100);
     *
     *   describe('A gray square with a file input beneath it. If the user loads a file, its data is written in black.');
     * }
     *
     * // Display the p5.File's data once it loads.
     * function displayData(file) {
     *   background(200);
     *
     *   // Display the p5.File's data, which looks like a random string of characters.
     *   text(file.data, 10, 10, 80, 80);
     * }
     */
  }

  if(typeof p5 !== 'undefined'){
    file(p5);
  }

  /**
   * @module DOM
   * @submodule DOM
   */


  class Element {
    width;
    height;
    elt;

    constructor(elt, pInst) {
      this.elt = elt;
      this._pInst = this._pixelsState = pInst;
      this._events = {};
      this.width = this.elt.offsetWidth;
      this.height = this.elt.offsetHeight;
    }

    /**
     * Removes the element, stops all audio/video streams, and removes all
     * callback functions.
     *
     * @example
     * let p;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a paragraph element.
     *   p = createP('p5*js');
     *   p.position(10, 10);
     *
     *   describe('The text "p5*js" written at the center of a gray square. ');
     * }
     *
     * // Remove the paragraph when the user double-clicks.
     * function doubleClicked() {
     *   p.remove();
     * }
     */
    remove() {
      // stop all audios/videos and detach all devices like microphone/camera etc
      // used as input/output for audios/videos.
      // if (this instanceof p5.MediaElement) {
      if(this.stop){
        this.stop();
        const sources = this.elt.srcObject;
        if (sources !== null) {
          const tracks = sources.getTracks();
          tracks.forEach(track => {
            track.stop();
          });
        }
      }

      // `this._pInst` is usually the p5 “sketch” object that owns the global
      // `_elements` array.  But when an element lives inside an off-screen
      // `p5.Graphics` layer, `this._pInst` is that wrapper Graphics object
      // instead.  The wrapper keeps a back–pointer (`_pInst`) to the real
      // sketch but has no `_elements` array of its own.

      let sketch = this._pInst;

      // If `sketch` doesn’t own an `_elements` array it means
      // we’re still at the graphics-layer “wrapper”.
      // Jump one level up to the real p5 sketch stored in sketch._pInst.

      if (sketch && !sketch._elements && sketch._pInst) {
        sketch = sketch._pInst;          // climb one level up
      }

      if (sketch && sketch._elements) {  // only if the array exists
        const i = sketch._elements.indexOf(this);
        if (i !== -1) sketch._elements.splice(i, 1);
      }

      // deregister events
      for (let ev in this._events) {
        this.elt.removeEventListener(ev, this._events[ev]);
      }
      if (this.elt && this.elt.parentNode) {
        this.elt.parentNode.removeChild(this.elt);
      }
    }

    /**
     * Attaches the element to a parent element.
     *
     * For example, a `&lt;div&gt;&lt;/div&gt;` element may be used as a box to
     * hold two pieces of text, a header and a paragraph. The
     * `&lt;div&gt;&lt;/div&gt;` is the parent element of both the header and
     * paragraph.
     *
     * The parameter `parent` can have one of three types. `parent` can be a
     * string with the parent element's ID, as in
     * `myElement.parent('container')`. It can also be another
     * <a href="#/p5.Element">p5.Element</a> object, as in
     * `myElement.parent(myDiv)`. Finally, `parent` can be an `HTMLElement`
     * object, as in `myElement.parent(anotherElement)`.
     *
     * Calling `myElement.parent()` without an argument returns the element's
     * parent.
     *
     * @param  {String|p5.Element|Object} parent ID, <a href="#/p5.Element">p5.Element</a>,
     *                                           or HTMLElement of desired parent element.
     * @chainable
     *
     * @example
     * function setup()  {
     *   background(200);
     *
     *   // Create a div element.
     *   let div = createDiv();
     *
     *   // Place the div in the top-left corner.
     *   div.position(10, 20);
     *
     *   // Set its width and height.
     *   div.size(80, 60);
     *
     *   // Set its background color to white
     *   div.style('background-color', 'white');
     *
     *   // Align any text to the center.
     *   div.style('text-align', 'center');
     *
     *   // Set its ID to "container".
     *   div.id('container');
     *
     *   // Create a paragraph element.
     *   let p = createP('p5*js');
     *
     *   // Make the div its parent
     *   // using its ID "container".
     *   p.parent('container');
     *
     *   describe('The text "p5*js" written in black at the center of a white rectangle. The rectangle is inside a gray square.');
     * }
     *
     * @example
     * function setup()  {
     *   background(200);
     *
     *   // Create rectangular div element.
     *   let div = createDiv();
     *
     *   // Place the div in the top-left corner.
     *   div.position(10, 20);
     *
     *   // Set its width and height.
     *   div.size(80, 60);
     *
     *   // Set its background color and align
     *   // any text to the center.
     *   div.style('background-color', 'white');
     *   div.style('text-align', 'center');
     *
     *   // Create a paragraph element.
     *   let p = createP('p5*js');
     *
     *   // Make the div its parent.
     *   p.parent(div);
     *
     *   describe('The text "p5*js" written in black at the center of a white rectangle. The rectangle is inside a gray square.');
     * }
     *
     * @example
     * function setup()  {
     *   background(200);
     *
     *   // Create rectangular div element.
     *   let div = createDiv();
     *
     *   // Place the div in the top-left corner.
     *   div.position(10, 20);
     *
     *   // Set its width and height.
     *   div.size(80, 60);
     *
     *   // Set its background color and align
     *   // any text to the center.
     *   div.style('background-color', 'white');
     *   div.style('text-align', 'center');
     *
     *   // Create a paragraph element.
     *   let p = createP('p5*js');
     *
     *   // Make the div its parent
     *   // using the underlying
     *   // HTMLElement.
     *   p.parent(div.elt);
     *
     *   describe('The text "p5*js" written in black at the center of a white rectangle. The rectangle is inside a gray square.');
     * }
     */
    /**
     * @return {p5.Element}
     */
    parent(p) {
      if (typeof p === 'undefined') {
        return this.elt.parentNode;
      }

      if (typeof p === 'string') {
        if (p[0] === '#') {
          p = p.substring(1);
        }
        p = document.getElementById(p);
      } else if (p instanceof Element) {
        p = p.elt;
      }
      p.appendChild(this.elt);
      return this;
    }

    /**
     * Attaches the element as a child of another element.
     *
     * `myElement.child()` accepts either a string ID, DOM node, or
     * <a href="#/p5.Element">p5.Element</a>. For example,
     * `myElement.child(otherElement)`. If no argument is provided, an array of
     * children DOM nodes is returned.
     *
     * @returns {Node[]} an array of child nodes.
     *
     * @example
     * // META:norender
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create the div elements.
     *   let div0 = createDiv('Parent');
     *   let div1 = createDiv('Child');
     *
     *   // Make div1 the child of div0
     *   // using the p5.Element.
     *   div0.child(div1);
     *
     *   describe('A gray square with the words "Parent" and "Child" written beneath it.');
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create the div elements.
     *   let div0 = createDiv('Parent');
     *   let div1 = createDiv('Child');
     *
     *   // Give div1 an ID.
     *   div1.id('apples');
     *
     *   // Make div1 the child of div0
     *   // using its ID.
     *   div0.child('apples');
     *
     *   describe('A gray square with the words "Parent" and "Child" written beneath it.');
     * }
     *
     * @example
     * // META:norender
     * // This example assumes there is a div already on the page
     * // with id "myChildDiv".
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create the div elements.
     *   let div0 = createDiv('Parent');
     *
     *   // Select the child element by its ID.
     *   let elt = document.getElementById('myChildDiv');
     *
     *   // Make div1 the child of div0
     *   // using its HTMLElement object.
     *   div0.child(elt);
     *
     *   describe('A gray square with the words "Parent" and "Child" written beneath it.');
     * }
     */
    /**
     * @param  {String|p5.Element} [child] the ID, DOM node, or <a href="#/p5.Element">p5.Element</a>
     *                         to add to the current element
     * @chainable
     */
    child(childNode) {
      if (typeof childNode === 'undefined') {
        return this.elt.childNodes;
      }
      if (typeof childNode === 'string') {
        if (childNode[0] === '#') {
          childNode = childNode.substring(1);
        }
        childNode = document.getElementById(childNode);
      } else if (childNode instanceof Element) {
        childNode = childNode.elt;
      }

      if (childNode instanceof HTMLElement) {
        this.elt.appendChild(childNode);
      }
      return this;
    }

    /**
     * Sets the inner HTML of the element, replacing any existing HTML.
     *
     * The second parameter, `append`, is optional. If `true` is passed, as in
     * `myElement.html('hi', true)`, the HTML is appended instead of replacing
     * existing HTML.
     *
     * If no arguments are passed, as in `myElement.html()`, the element's inner
     * HTML is returned.
     *
     * @for p5.Element
     * @returns {String} the inner HTML of the element
     *
     * @example
     * // META:norender
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create the div element and set its size.
     *   let div = createDiv('');
     *   div.size(100, 100);
     *
     *   // Set the inner HTML to "hi".
     *   div.html('hi');
     *
     *   describe('A gray square with the word "hi" written beneath it.');
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create the div element and set its size.
     *   let div = createDiv('Hello ');
     *   div.size(100, 100);
     *
     *   // Append "World" to the div's HTML.
     *   div.html('World', true);
     *
     *   describe('A gray square with the text "Hello World" written beneath it.');
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create the div element.
     *   let div = createDiv('Hello');
     *
     *   // Prints "Hello" to the console.
     *   print(div.html());
     *
     *   describe('A gray square with the word "Hello!" written beneath it.');
     * }
     */
    /**
     * @param  {String} [html] the HTML to be placed inside the element
     * @param  {Boolean} [append] whether to append HTML to existing
     * @chainable
     */
    html(...args) {
      if (args.length === 0) {
        return this.elt.innerHTML;
      } else if (args[1]) {
        this.elt.insertAdjacentHTML('beforeend', args[0]);
        return this;
      } else {
        this.elt.innerHTML = args[0];
        return this;
      }
    }

    /**
     * Sets the element's ID using a given string.
     *
     * Calling `myElement.id()` without an argument returns its ID as a string.
     *
     * @param  {String} id ID of the element.
     * @chainable
     *
     * @example
     * function setup() {
     *   // Create a canvas element and
     *   // assign it to cnv.
     *   let cnv = createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the canvas' ID
     *   // to "mycanvas".
     *   cnv.id('mycanvas');
     *
     *   // Get the canvas' ID.
     *   let id = cnv.id();
     *   text(id, 24, 54);
     *
     *   describe('The text "mycanvas" written in black on a gray background.');
     * }
     */
    /**
     * @return {String} ID of the element.
     */
    id(id) {
      if (typeof id === 'undefined') {
        return this.elt.id;
      }

      this.elt.id = id;
      this.width = this.elt.offsetWidth;
      this.height = this.elt.offsetHeight;
      return this;
    }

    /**
     * Adds a
     * <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class" target="_blank">class attribute</a>
     * to the element using a given string.
     *
     * Calling `myElement.class()` without an argument returns a string with its current classes.
     *
     * @param  {String} class class to add.
     * @chainable
     *
     * @example
     * function setup() {
     *   // Create a canvas element and
     *   // assign it to cnv.
     *   let cnv = createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Add the class "small" to the
     *   // canvas element.
     *   cnv.class('small');
     *
     *   // Get the canvas element's class
     *   // and display it.
     *   let c = cnv.class();
     *   text(c, 35, 54);
     *
     *   describe('The word "small" written in black on a gray canvas.');
     *
     * }
     */
    /**
     * @return {String} element's classes, if any.
     */
    class(c) {
      if (typeof c === 'undefined') {
        return this.elt.className;
      }

      this.elt.className = c;
      return this;
    }

    /**
     *
     * Adds a class to the element.
     *
     * @for p5.Element
     * @param  {String} class name of class to add.
     * @chainable
     *
     * @example
     * // META:norender
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a div element.
     *   let div = createDiv('div');
     *
     *   // Add a class to the div.
     *   div.addClass('myClass');
     *
     *   describe('A gray square.');
     * }
     */
    addClass(c) {
      if (this.elt.className) {
        if (!this.hasClass(c)) {
          this.elt.className = this.elt.className + ' ' + c;
        }
      } else {
        this.elt.className = c;
      }
      return this;
    }

    /**
     * Removes a class from the element.
     *
     * @param  {String} class name of class to remove.
     * @chainable
     *
     * @example
     * // META:norender
     * // In this example, a class is set when the div is created
     * // and removed when mouse is pressed. This could link up
     * // with a CSS style rule to toggle style properties.
     *
     * let div;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a div element.
     *   div = createDiv('div');
     *
     *   // Add a class to the div.
     *   div.addClass('myClass');
     *
     *   describe('A gray square.');
     * }
     *
     * // Remove 'myClass' from the div when the user presses the mouse.
     * function mousePressed() {
     *   div.removeClass('myClass');
     * }
     */
    removeClass(c) {
      // Note: Removing a class that does not exist does NOT throw an error in classList.remove method
      this.elt.classList.remove(c);
      return this;
    }

    /**
     * Checks if a class is already applied to element.
     *
     * @returns {boolean} a boolean value if element has specified class.
     * @param c {String} name of class to check.
     *
     * @example
     * // META:norender
     * let div;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a div element.
     *   div = createDiv('div');
     *
     *   // Add the class 'show' to the div.
     *   div.addClass('show');
     *
     *   describe('A gray square.');
     * }
     *
     * // Toggle the class 'show' when the mouse is pressed.
     * function mousePressed() {
     *   if (div.hasClass('show')) {
     *     div.addClass('show');
     *   } else {
     *     div.removeClass('show');
     *   }
     * }
     */
    hasClass(c) {
      return this.elt.classList.contains(c);
    }

    /**
     * Toggles whether a class is applied to the element.
     *
     * @param c {String} class name to toggle.
     * @chainable
     *
     * @example
     * // META:norender
     * let div;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a div element.
     *   div = createDiv('div');
     *
     *   // Add the 'show' class to the div.
     *   div.addClass('show');
     *
     *   describe('A gray square.');
     * }
     *
     * // Toggle the 'show' class when the mouse is pressed.
     * function mousePressed() {
     *   div.toggleClass('show');
     * }
     */
    toggleClass(c) {
      // classList also has a toggle() method, but we cannot use that yet as support is unclear.
      // See https://github.com/processing/p5.js/issues/3631
      // this.elt.classList.toggle(c);
      if (this.elt.classList.contains(c)) {
        this.elt.classList.remove(c);
      } else {
        this.elt.classList.add(c);
      }
      return this;
    }

    /**
     * Centers the element either vertically, horizontally, or both.
     *
     * `center()` will center the element relative to its parent or according to
     * the page's body if the element has no parent.
     *
     * If no argument is passed, as in `myElement.center()` the element is aligned
     * both vertically and horizontally.
     *
     * @param  {String} [align] passing 'vertical', 'horizontal' aligns element accordingly
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create the div element and style it.
     *   let div = createDiv('');
     *   div.size(10, 10);
     *   div.style('background-color', 'orange');
     *
     *   // Center the div relative to the page's body.
     *   div.center();
     *
     *   describe('A gray square and an orange rectangle. The rectangle is at the center of the page.');
     * }
     */
    center(align) {
      const style = this.elt.style.display;
      const hidden = this.elt.style.display === 'none';
      const parentHidden = this.parent().style.display === 'none';
      const pos = { x: this.elt.offsetLeft, y: this.elt.offsetTop };

      if (hidden) this.show();
      if (parentHidden) this.parent().show();
      this.elt.style.display = 'block';

      this.position(0, 0);
      const wOffset = Math.abs(this.parent().offsetWidth - this.elt.offsetWidth);
      const hOffset = Math.abs(
        this.parent().offsetHeight - this.elt.offsetHeight
      );

      if (align === 'both' || align === undefined) {
        this.position(
          wOffset / 2 + this.parent().offsetLeft,
          hOffset / 2 + this.parent().offsetTop
        );
      } else if (align === 'horizontal') {
        this.position(wOffset / 2 + this.parent().offsetLeft, pos.y);
      } else if (align === 'vertical') {
        this.position(pos.x, hOffset / 2 + this.parent().offsetTop);
      }

      this.style('display', style);
      if (hidden) this.hide();
      if (parentHidden) this.parent().hide();

      return this;
    }

    /**
     * Sets the element's position.
     *
     * The first two parameters, `x` and `y`, set the element's position relative
     * to the top-left corner of the web page.
     *
     * The third parameter, `positionType`, is optional. It sets the element's
     * <a target="_blank"
     * href="https://developer.mozilla.org/en-US/docs/Web/CSS/position">positioning scheme</a>.
     * `positionType` is a string that can be either `'static'`, `'fixed'`,
     * `'relative'`, `'sticky'`, `'initial'`, or `'inherit'`.
     *
     * If no arguments passed, as in `myElement.position()`, the method returns
     * the element's position in an object, as in `{ x: 0, y: 0 }`.
     *
     * @returns {Object} object of form `{ x: 0, y: 0 }` containing the element's position.
     *
     * @example
     * // META:norender
     * function setup() {
     *   let cnv = createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Positions the canvas 50px to the right and 100px
     *   // below the top-left corner of the window.
     *   cnv.position(50, 100);
     *
     *   describe('A gray square that is 50 pixels to the right and 100 pixels down from the top-left corner of the web page.');
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   let cnv = createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Positions the canvas at the top-left corner
     *   // of the window with a 'fixed' position type.
     *   cnv.position(0, 0, 'fixed');
     *
     *   describe('A gray square in the top-left corner of the web page.');
     * }
     */
    /**
     * @param  {Number} [x] x-position relative to top-left of window (optional)
     * @param  {Number} [y] y-position relative to top-left of window (optional)
     * @param  {String} [positionType] it can be static, fixed, relative, sticky, initial or inherit (optional)
     * @chainable
     */
    position(...args) {
      if (args.length === 0) {
        return { x: this.elt.offsetLeft, y: this.elt.offsetTop };
      } else {
        let positionType = 'absolute';
        if (
          args[2] === 'static' ||
          args[2] === 'fixed' ||
          args[2] === 'relative' ||
          args[2] === 'sticky' ||
          args[2] === 'initial' ||
          args[2] === 'inherit'
        ) {
          positionType = args[2];
        }
        this.elt.style.position = positionType;
        this.elt.style.left = args[0] + 'px';
        this.elt.style.top = args[1] + 'px';
        this.x = args[0];
        this.y = args[1];
        return this;
      }
    }

    /**
     * Shows the current element.
     *
     * @chainable
     *
     * @example
     * let p;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a paragraph element and hide it.
     *   p = createP('p5*js');
     *   p.position(10, 10);
     *   p.hide();
     *
     *   describe('A gray square. The text "p5*js" appears when the user double-clicks the square.');
     * }
     *
     * // Show the paragraph when the user double-clicks.
     * function doubleClicked() {
     *   p.show();
     * }
     */
    show() {
      this.elt.style.display = 'block';
      return this;
    }

    /**
     * Hides the current element.
     *
     * @chainable
     *
     * @example
     * let p;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a paragraph element.
     *   p = createP('p5*js');
     *   p.position(10, 10);
     *
     *   describe('The text "p5*js" at the center of a gray square. The text disappears when the user double-clicks the square.');
     * }
     *
     * // Hide the paragraph when the user double-clicks.
     * function doubleClicked() {
     *   p.hide();
     * }
     */
    hide() {
      this.elt.style.display = 'none';
      return this;
    }

    /**
     * Sets the element's width and height.
     *
     * Calling `myElement.size()` without an argument returns the element's size
     * as an object with the properties `width` and `height`. For example,
     *  `{ width: 20, height: 10 }`.
     *
     * The first parameter, `width`, is optional. It's a number used to set the
     * element's width. Calling `myElement.size(10)`
     *
     * The second parameter, 'height`, is also optional. It's a
     * number used to set the element's height. For example, calling
     * `myElement.size(20, 10)` sets the element's width to 20 pixels and height
     * to 10 pixels.
     *
     * The constant `AUTO` can be used to adjust one dimension at a time while
     * maintaining the aspect ratio, which is `width / height`. For example,
     * consider an element that's 200 pixels wide and 100 pixels tall. Calling
     * `myElement.size(20, AUTO)` sets the width to 20 pixels and height to 10
     * pixels.
     *
     * Note: In the case of elements that need to load data, such as images, wait
     * to call `myElement.size()` until after the data loads.
     *
     * @return {Object} width and height of the element in an object.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a pink div element and place it at the top-left corner.
     *   let div = createDiv();
     *   div.position(10, 10);
     *   div.style('background-color', 'deeppink');
     *
     *   // Set the div's width to 80 pixels and height to 20 pixels.
     *   div.size(80, 20);
     *
     *   describe('A gray square with a pink rectangle near its top.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a pink div element and place it at the top-left corner.
     *   let div = createDiv();
     *   div.position(10, 10);
     *   div.style('background-color', 'deeppink');
     *
     *   // Set the div's width to 80 pixels and height to 40 pixels.
     *   div.size(80, 40);
     *
     *   // Get the div's size as an object.
     *   let s = div.size();
     *
     *   // Display the div's dimensions.
     *   div.html(`${s.width} x ${s.height}`);
     *
     *   describe('A gray square with a pink rectangle near its top. The text "80 x 40" is written within the rectangle.');
     * }
     *
     * @example
     * let img1;
     * let img2;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Load an image of an astronaut on the moon
     *   // and place it at the top-left of the canvas.
     *   img1 = createImg(
     *     'assets/moonwalk.jpg',
     *     'An astronaut walking on the moon',
     *     ''
     *   );
     *   img1.position(0, 0);
     *
     *   // Load an image of an astronaut on the moon
     *   // and place it at the top-left of the canvas.
     *   // Resize the image once it's loaded.
     *   img2 = createImg(
     *     'assets/moonwalk.jpg',
     *     'An astronaut walking on the moon',
     *     '',
     *     resizeImage
     *   );
     *   img2.position(0, 0);
     *
     *   describe('A gray square two copies of a space image at the top-left. The copy in front is smaller.');
     * }
     *
     * // Resize img2 and keep its aspect ratio.
     * function resizeImage() {
     *   img2.size(50, AUTO);
     * }
     */
    /**
     * @param  {(Number|AUTO)} [w]   width of the element, either AUTO, or a number.
     * @param  {(Number|AUTO)} [h] height of the element, either AUTO, or a number.
     * @chainable
     */
    size(w, h) {
      if (arguments.length === 0) {
        return { width: this.elt.offsetWidth, height: this.elt.offsetHeight };
      } else {
        let aW = w;
        let aH = h;
        const AUTO$1 = AUTO;
        if (aW !== AUTO$1 || aH !== AUTO$1) {
          if (aW === AUTO$1) {
            aW = h * this.width / this.height;
          } else if (aH === AUTO$1) {
            aH = w * this.height / this.width;
          }
          // set diff for cnv vs normal div
          if (this.elt instanceof HTMLCanvasElement) {
            const j = {};
            const k = this.elt.getContext('2d');
            let prop;
            for (prop in k) {
              j[prop] = k[prop];
            }
            this.elt.setAttribute('width', aW * this._pInst._pixelDensity);
            this.elt.setAttribute('height', aH * this._pInst._pixelDensity);
            this.elt.style.width = aW + 'px';
            this.elt.style.height = aH + 'px';
            this._pInst.scale(
              this._pInst._pixelDensity,
              this._pInst._pixelDensity
            );
            for (prop in j) {
              this.elt.getContext('2d')[prop] = j[prop];
            }
          } else {
            this.elt.style.width = aW + 'px';
            this.elt.style.height = aH + 'px';
            this.elt.width = aW;
            this.elt.height = aH;
          }
          this.width = aW;
          this.height = aH;
          if (this._pInst && this._pInst._curElement) {
            // main canvas associated with p5 instance
            if (this._pInst._curElement.elt === this.elt) {
              this._pInst.width = aW;
              this._pInst.height = aH;
            }
          }
        }
        return this;
      }
    }

    /**
     * Applies a style to the element by adding a
     * <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax" target="_blank">CSS declaration</a>.
     *
     * The first parameter, `property`, is a string. If the name of a style
     * property is passed, as in `myElement.style('color')`, the method returns
     * the current value as a string or `null` if it hasn't been set. If a
     * `property:style` string is passed, as in
     * `myElement.style('color:deeppink')`, the method sets the style `property`
     * to `value`.
     *
     * The second parameter, `value`, is optional. It sets the property's value.
     * `value` can be a string, as in
     * `myElement.style('color', 'deeppink')`, or a
     * <a href="#/p5.Color">p5.Color</a> object, as in
     * `myElement.style('color', myColor)`.
     *
     * @param  {String} property style property to set.
     * @returns {String} value of the property.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a paragraph element and set its font color to "deeppink".
     *   let p = createP('p5*js');
     *   p.position(25, 20);
     *   p.style('color', 'deeppink');
     *
     *   describe('The text p5*js written in pink on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object.
     *   let c = color('deeppink');
     *
     *   // Create a paragraph element and set its font color using a p5.Color object.
     *   let p = createP('p5*js');
     *   p.position(25, 20);
     *   p.style('color', c);
     *
     *   describe('The text p5*js written in pink on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a paragraph element and set its font color to "deeppink"
     *   // using property:value syntax.
     *   let p = createP('p5*js');
     *   p.position(25, 20);
     *   p.style('color:deeppink');
     *
     *   describe('The text p5*js written in pink on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an empty paragraph element and set its font color to "deeppink".
     *   let p = createP();
     *   p.position(5, 5);
     *   p.style('color', 'deeppink');
     *
     *   // Get the element's color as an  RGB color string.
     *   let c = p.style('color');
     *
     *   // Set the element's inner HTML using the RGB color string.
     *   p.html(c);
     *
     *   describe('The text "rgb(255, 20, 147)" written in pink on a gray background.');
     * }
     */
    /**
     * @param  {String} property
     * @param  {String|p5.Color} value value to assign to the property.
     * @return {String} value of the property.
     * @chainable
     */
    style(prop, val) {
      const self = this;

      if (val instanceof Color) {
        val = val.toString();
      }

      if (typeof val === 'undefined') {
        if (prop.indexOf(':') === -1) {
          // no value set, so assume requesting a value
          let styles = window.getComputedStyle(self.elt);
          let style = styles.getPropertyValue(prop);
          return style;
        } else {
          // value set using `:` in a single line string
          const attrs = prop.split(';');
          for (let i = 0; i < attrs.length; i++) {
            const parts = attrs[i].split(':');
            if (parts[0] && parts[1]) {
              this.elt.style[parts[0].trim()] = parts[1].trim();
            }
          }
        }
      } else {
        // input provided as key,val pair
        this.elt.style[prop] = val;
        if (
          prop === 'width' ||
          prop === 'height' ||
          prop === 'left' ||
          prop === 'top'
        ) {
          let styles = window.getComputedStyle(self.elt);
          let styleVal = styles.getPropertyValue(prop);
          let numVal = styleVal.replace(/[^\d.]/g, '');
          this[prop] = Math.round(parseFloat(numVal, 10));
        }
      }
      return this;
    }

    /* Helper method called by p5.Element.style() */
    _translate(...args) {
      this.elt.style.position = 'absolute';
      // save out initial non-translate transform styling
      let transform = '';
      if (this.elt.style.transform) {
        transform = this.elt.style.transform.replace(/translate3d\(.*\)/g, '');
        transform = transform.replace(/translate[X-Z]?\(.*\)/g, '');
      }
      if (args.length === 2) {
        this.elt.style.transform =
          'translate(' + args[0] + 'px, ' + args[1] + 'px)';
      } else if (args.length > 2) {
        this.elt.style.transform =
          'translate3d(' +
          args[0] +
          'px,' +
          args[1] +
          'px,' +
          args[2] +
          'px)';
        if (args.length === 3) {
          this.elt.parentElement.style.perspective = '1000px';
        } else {
          this.elt.parentElement.style.perspective = args[3] + 'px';
        }
      }
      // add any extra transform styling back on end
      this.elt.style.transform += transform;
      return this;
    }

    /* Helper method called by p5.Element.style() */
    _rotate(...args) {
      // save out initial non-rotate transform styling
      let transform = '';
      if (this.elt.style.transform) {
        transform = this.elt.style.transform.replace(/rotate3d\(.*\)/g, '');
        transform = transform.replace(/rotate[X-Z]?\(.*\)/g, '');
      }

      if (args.length === 1) {
        this.elt.style.transform = 'rotate(' + args[0] + 'deg)';
      } else if (args.length === 2) {
        this.elt.style.transform =
          'rotate(' + args[0] + 'deg, ' + args[1] + 'deg)';
      } else if (args.length === 3) {
        this.elt.style.transform = 'rotateX(' + args[0] + 'deg)';
        this.elt.style.transform += 'rotateY(' + args[1] + 'deg)';
        this.elt.style.transform += 'rotateZ(' + args[2] + 'deg)';
      }
      // add remaining transform back on
      this.elt.style.transform += transform;
      return this;
    }

    /**
     * Adds an
     * <a href="https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML/Getting_started#attributes" target="_blank">attribute</a>
     * to the element.
     *
     * This method is useful for advanced tasks. Most commonly-used attributes,
     * such as `id`, can be set with their dedicated methods. For example,
     * `nextButton.id('next')` sets an element's `id` attribute. Calling
     * `nextButton.attribute('id', 'next')` has the same effect.
     *
     * The first parameter, `attr`, is the attribute's name as a string. Calling
     * `myElement.attribute('align')` returns the attribute's current value as a
     * string or `null` if it hasn't been set.
     *
     * The second parameter, `value`, is optional. It's a string used to set the
     * attribute's value. For example, calling
     * `myElement.attribute('align', 'center')` sets the element's horizontal
     * alignment to `center`.
     *
     * @return {String} value of the attribute.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a container div element and place it at the top-left corner.
     *   let container = createDiv();
     *   container.position(0, 0);
     *
     *   // Create a paragraph element and place it within the container.
     *   // Set its horizontal alignment to "left".
     *   let p1 = createP('hi');
     *   p1.parent(container);
     *   p1.attribute('align', 'left');
     *
     *   // Create a paragraph element and place it within the container.
     *   // Set its horizontal alignment to "center".
     *   let p2 = createP('hi');
     *   p2.parent(container);
     *   p2.attribute('align', 'center');
     *
     *   // Create a paragraph element and place it within the container.
     *   // Set its horizontal alignment to "right".
     *   let p3 = createP('hi');
     *   p3.parent(container);
     *   p3.attribute('align', 'right');
     *
     *   describe('A gray square with the text "hi" written on three separate lines, each placed further to the right.');
     * }
     */
    /**
     * @param  {String} attr       attribute to set.
     * @param  {String} value      value to assign to the attribute.
     * @chainable
     */
    attribute(attr, value) {
      //handling for checkboxes and radios to ensure options get
      //attributes not divs
      if (
        this.elt.firstChild != null &&
        (this.elt.firstChild.type === 'checkbox' ||
          this.elt.firstChild.type === 'radio')
      ) {
        if (typeof value === 'undefined') {
          return this.elt.firstChild.getAttribute(attr);
        } else {
          for (let i = 0; i < this.elt.childNodes.length; i++) {
            this.elt.childNodes[i].setAttribute(attr, value);
          }
        }
      } else if (typeof value === 'undefined') {
        return this.elt.getAttribute(attr);
      } else {
        this.elt.setAttribute(attr, value);
        return this;
      }
    }

    /**
     * Removes an attribute from the element.
     *
     * The parameter `attr` is the attribute's name as a string. For example,
     * calling `myElement.removeAttribute('align')` removes its `align`
     * attribute if it's been set.
     *
     * @param  {String} attr       attribute to remove.
     * @chainable
     *
     * @example
     * let p;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a paragraph element and place it in the center of the canvas.
     *   // Set its "align" attribute to "center".
     *   p = createP('hi');
     *   p.position(0, 20);
     *   p.attribute('align', 'center');
     *
     *   describe('The text "hi" written in black at the center of a gray square. The text moves to the left edge when double-clicked.');
     * }
     *
     * // Remove the 'align' attribute when the user double-clicks the paragraph.
     * function doubleClicked() {
     *   p.removeAttribute('align');
     * }
     */
    removeAttribute(attr) {
      if (
        this.elt.firstChild != null &&
        (this.elt.firstChild.type === 'checkbox' ||
          this.elt.firstChild.type === 'radio')
      ) {
        for (let i = 0; i < this.elt.childNodes.length; i++) {
          this.elt.childNodes[i].removeAttribute(attr);
        }
      }
      this.elt.removeAttribute(attr);
      return this;
    }

    /**
     * Returns or sets the element's value.
     *
     * Calling `myElement.value()` returns the element's current value.
     *
     * The parameter, `value`, is an optional number or string. If provided,
     * as in `myElement.value(123)`, it's used to set the element's value.
     *
     * @return {String|Number} value of the element.
     *
     * @example
     * let input;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a text input and place it beneath the canvas.
     *   // Set its default value to "hello".
     *   input = createInput('hello');
     *   input.position(0, 100);
     *
     *   describe('The text from an input box is displayed on a gray square.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Use the input's value to display a message.
     *   let msg = input.value();
     *   text(msg, 0, 55);
     * }
     *
     * @example
     * let input;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a text input and place it beneath the canvas.
     *   // Set its default value to "hello".
     *   input = createInput('hello');
     *   input.position(0, 100);
     *
     *   describe('The text from an input box is displayed on a gray square. The text resets to "hello" when the user double-clicks the square.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Use the input's value to display a message.
     *   let msg = input.value();
     *   text(msg, 0, 55);
     * }
     *
     * // Reset the input's value.
     * function doubleClicked() {
     *   input.value('hello');
     * }
     */
    /**
     * @param  {String|Number}     value
     * @chainable
     */
    value(...args) {
      if (args.length > 0) {
        this.elt.value = args[0];
        return this;
      } else {
        if (this.elt.type === 'range') {
          return parseFloat(this.elt.value);
        } else return this.elt.value;
      }
    }

    /**
     * Calls a function when the mouse is pressed over the element.
     *
     * Calling `myElement.mousePressed(false)` disables the function.
     *
     * Note: Some mobile browsers may also trigger this event when the element
     * receives a quick tap.
     *
     * @param  {Function|Boolean} fxn function to call when the mouse is
     *                                pressed over the element.
     *                                `false` disables the function.
     * @chainable
     *
     * @example
     * function setup() {
     *   // Create a canvas element and
     *   // assign it to cnv.
     *   let cnv = createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Call randomColor() when the canvas
     *   // is pressed.
     *   cnv.mousePressed(randomColor);
     *
     *   describe('A gray square changes color when the mouse is pressed.');
     * }
     *
     * // Paint the background either
     * // red, yellow, blue, or green.
     * function randomColor() {
     *   let c = random(['red', 'yellow', 'blue', 'green']);
     *   background(c);
     * }
     */
    mousePressed(fxn) {
      // Prepend the mouse property setters to the event-listener.
      // This is required so that mouseButton is set correctly prior to calling the callback (fxn).
      // For details, see https://github.com/processing/p5.js/issues/3087.
      const eventPrependedFxn = function (event) {
        this._pInst.mouseIsPressed = true;
        this._pInst._activePointers.set(event.pointerId, event);
        this._pInst._setMouseButton(event);
        this._pInst._updatePointerCoords(event);
        // Pass along the return-value of the callback:
        return fxn.call(this, event);
      };
      // Pass along the event-prepended form of the callback.
      Element._adjustListener('pointerdown', eventPrependedFxn, this);
      return this;
    }

    /**
     * Calls a function when the mouse is pressed twice over the element.
     *
     * Calling `myElement.doubleClicked(false)` disables the function.
     *
     * @param  {Function|Boolean} fxn function to call when the mouse is
     *                                double clicked over the element.
     *                                `false` disables the function.
     * @chainable
     *
     * @example
     * function setup() {
     *   // Create a canvas element and
     *   // assign it to cnv.
     *   let cnv = createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Call randomColor() when the
     *   // canvas is double-clicked.
     *   cnv.doubleClicked(randomColor);
     *
     *   describe('A gray square changes color when the user double-clicks the canvas.');
     * }
     *
     * // Paint the background either
     * // red, yellow, blue, or green.
     * function randomColor() {
     *   let c = random(['red', 'yellow', 'blue', 'green']);
     *   background(c);
     * }
     */
    doubleClicked(fxn) {
      Element._adjustListener('dblclick', fxn, this);
      return this;
    }

    /**
     * Calls a function when the mouse wheel scrolls over the element.
     *
     * The callback function, `fxn`, is passed an `event` object. `event` has
     * two numeric properties, `deltaY` and `deltaX`. `event.deltaY` is
     * negative if the mouse wheel rotates away from the user. It's positive if
     * the mouse wheel rotates toward the user. `event.deltaX` is positive if
     * the mouse wheel moves to the right. It's negative if the mouse wheel moves
     * to the left.
     *
     * Calling `myElement.mouseWheel(false)` disables the function.
     *
     * @param  {Function|Boolean} fxn function to call when the mouse wheel is
     *                                scrolled over the element.
     *                                `false` disables the function.
     * @chainable
     *
     * @example
     * function setup() {
     *   // Create a canvas element and
     *   // assign it to cnv.
     *   let cnv = createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Call randomColor() when the
     *   // mouse wheel moves.
     *   cnv.mouseWheel(randomColor);
     *
     *   describe('A gray square changes color when the user scrolls the mouse wheel over the canvas.');
     * }
     *
     * // Paint the background either
     * // red, yellow, blue, or green.
     * function randomColor() {
     *   let c = random(['red', 'yellow', 'blue', 'green']);
     *   background(c);
     * }
     *
     * @example
     * function setup() {
     *   // Create a canvas element and
     *   // assign it to cnv.
     *   let cnv = createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Call changeBackground() when the
     *   // mouse wheel moves.
     *   cnv.mouseWheel(changeBackground);
     *
     *   describe('A gray square. When the mouse wheel scrolls over the square, it changes color and displays shapes.');
     * }
     *
     * function changeBackground(event) {
     *   // Change the background color
     *   // based on deltaY.
     *   if (event.deltaY > 0) {
     *     background('deeppink');
     *   } else if (event.deltaY < 0) {
     *     background('cornflowerblue');
     *   } else {
     *     background(200);
     *   }
     *
     *   // Draw a shape based on deltaX.
     *   if (event.deltaX > 0) {
     *     circle(50, 50, 20);
     *   } else if (event.deltaX < 0) {
     *     square(40, 40, 20);
     *   }
     * }
     */
    mouseWheel(fxn) {
      Element._adjustListener('wheel', fxn, this);
      return this;
    }

    /**
     * Calls a function when the mouse is released over the element.
     *
     * Calling `myElement.mouseReleased(false)` disables the function.
     *
     * Note: Some mobile browsers may also trigger this event when the element
     * receives a quick tap.
     *
     * @param  {Function|Boolean} fxn function to call when the mouse is
     *                                pressed over the element.
     *                                `false` disables the function.
     * @chainable
     *
     * @example
     * function setup() {
     *   // Create a canvas element and
     *   // assign it to cnv.
     *   let cnv = createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Call randomColor() when a
     *   // mouse press ends.
     *   cnv.mouseReleased(randomColor);
     *
     *   describe('A gray square changes color when the user releases a mouse press.');
     * }
     *
     * // Paint the background either
     * // red, yellow, blue, or green.
     * function randomColor() {
     *   let c = random(['red', 'yellow', 'blue', 'green']);
     *   background(c);
     * }
     */
    mouseReleased(fxn) {
      Element._adjustListener('pointerup', fxn, this);
      return this;
    }

    /**
     * Calls a function when the mouse is pressed and released over the element.
     *
     * Calling `myElement.mouseReleased(false)` disables the function.
     *
     * Note: Some mobile browsers may also trigger this event when the element
     * receives a quick tap.
     *
     * @param  {Function|Boolean} fxn function to call when the mouse is
     *                                pressed and released over the element.
     *                                `false` disables the function.
     * @chainable
     *
     * @example
     * function setup() {
     *   // Create a canvas element and
     *   // assign it to cnv.
     *   let cnv = createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Call randomColor() when a
     *   // mouse press ends.
     *   cnv.mouseClicked(randomColor);
     *
     *   describe('A gray square changes color when the user releases a mouse press.');
     * }
     *
     * // Paint the background either
     * // red, yellow, blue, or green.
     * function randomColor() {
     *   let c = random(['red', 'yellow', 'blue', 'green']);
     *   background(c);
     * }
     */
    mouseClicked(fxn) {
      Element._adjustListener('click', fxn, this);
      return this;
    }

    /**
     * Calls a function when the mouse moves over the element.
     *
     * Calling `myElement.mouseMoved(false)` disables the function.
     *
     * @param  {Function|Boolean} fxn function to call when the mouse
     *                                moves over the element.
     *                                `false` disables the function.
     * @chainable
     *
     * @example
     * function setup() {
     *   // Create a canvas element and
     *   // assign it to cnv.
     *   let cnv = createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Call randomColor() when the
     *   // mouse moves.
     *   cnv.mouseMoved(randomColor);
     *
     *   describe('A gray square changes color when the mouse moves over the canvas.');
     * }
     *
     * // Paint the background either
     * // red, yellow, blue, or green.
     * function randomColor() {
     *   let c = random(['red', 'yellow', 'blue', 'green']);
     *   background(c);
     * }
     */
    mouseMoved(fxn) {
      Element._adjustListener('pointermove', fxn, this);
      return this;
    }

    /**
     * Calls a function when the mouse moves onto the element.
     *
     * Calling `myElement.mouseOver(false)` disables the function.
     *
     * @param  {Function|Boolean} fxn function to call when the mouse
     *                                moves onto the element.
     *                                `false` disables the function.
     * @chainable
     *
     * @example
     * function setup() {
     *   // Create a canvas element and
     *   // assign it to cnv.
     *   let cnv = createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Call randomColor() when the
     *   // mouse moves onto the canvas.
     *   cnv.mouseOver(randomColor);
     *
     *   describe('A gray square changes color when the mouse moves onto the canvas.');
     * }
     *
     * // Paint the background either
     * // red, yellow, blue, or green.
     * function randomColor() {
     *   let c = random(['red', 'yellow', 'blue', 'green']);
     *   background(c);
     * }
     */
    mouseOver(fxn) {
      Element._adjustListener('pointerover', fxn, this);
      return this;
    }

    /**
     * Calls a function when the mouse moves off the element.
     *
     * Calling `myElement.mouseOut(false)` disables the function.
     *
     * @param  {Function|Boolean} fxn function to call when the mouse
     *                                moves off the element.
     *                                `false` disables the function.
     * @chainable
     *
     * @example
     * function setup() {
     *   // Create a canvas element and
     *   // assign it to cnv.
     *   let cnv = createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Call randomColor() when the
     *   // mouse moves off the canvas.
     *   cnv.mouseOut(randomColor);
     *
     *   describe('A gray square changes color when the mouse moves off the canvas.');
     * }
     *
     * // Paint the background either
     * // red, yellow, blue, or green.
     * function randomColor() {
     *   let c = random(['red', 'yellow', 'blue', 'green']);
     *   background(c);
     * }
     */
    mouseOut(fxn) {
      Element._adjustListener('pointerout', fxn, this);
      return this;
    }

    /**
     * Calls a function when a file is dragged over the element.
     *
     * Calling `myElement.dragOver(false)` disables the function.
     *
     * @param  {Function|Boolean} fxn function to call when the file is
     *                                dragged over the element.
     *                                `false` disables the function.
     * @chainable
     *
     * @example
     * // Drag a file over the canvas to test.
     *
     * function setup() {
     *   // Create a canvas element and
     *   // assign it to cnv.
     *   let cnv = createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Call helloFile() when a
     *   // file is dragged over
     *   // the canvas.
     *   cnv.dragOver(helloFile);
     *
     *   describe('A gray square. The text "hello, file" appears when a file is dragged over the square.');
     * }
     *
     * function helloFile() {
     *   text('hello, file', 50, 50);
     * }
     */
    dragOver(fxn) {
      Element._adjustListener('dragover', fxn, this);
      return this;
    }

    /**
     * Calls a function when a file is dragged off the element.
     *
     * Calling `myElement.dragLeave(false)` disables the function.
     *
     * @param  {Function|Boolean} fxn function to call when the file is
     *                                dragged off the element.
     *                                `false` disables the function.
     * @chainable
     * @example
     * // Drag a file over, then off
     * // the canvas to test.
     *
     * function setup() {
     *   // Create a canvas element and
     *   // assign it to cnv.
     *   let cnv = createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Call byeFile() when a
     *   // file is dragged over,
     *   // then off the canvas.
     *   cnv.dragLeave(byeFile);
     *
     *   describe('A gray square. The text "bye, file" appears when a file is dragged over, then off the square.');
     * }
     *
     * function byeFile() {
     *   text('bye, file', 50, 50);
     * }
     */
    dragLeave(fxn) {
      Element._adjustListener('dragleave', fxn, this);
      return this;
    }

    /**
     * Calls a function when the element changes.
     *
     * Calling `myElement.changed(false)` disables the function.
     *
     * @param  {Function|Boolean} fxn function to call when the element changes.
     *                                `false` disables the function.
     * @chainable
     *
     * @example
     * let dropdown;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a dropdown menu and add a few color options.
     *   dropdown = createSelect();
     *   dropdown.position(0, 0);
     *   dropdown.option('red');
     *   dropdown.option('green');
     *   dropdown.option('blue');
     *
     *   // Call paintBackground() when the color option changes.
     *   dropdown.changed(paintBackground);
     *
     *   describe('A gray square with a dropdown menu at the top. The square changes color when an option is selected.');
     * }
     *
     * // Paint the background with the selected color.
     * function paintBackground() {
     *   let c = dropdown.value();
     *   background(c);
     * }
     *
     * @example
     * let checkbox;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a checkbox and place it beneath the canvas.
     *   checkbox = createCheckbox(' circle');
     *   checkbox.position(0, 100);
     *
     *   // Call repaint() when the checkbox changes.
     *   checkbox.changed(repaint);
     *
     *   describe('A gray square with a checkbox underneath it that says "circle". A white circle appears when the box is checked and disappears otherwise.');
     * }
     *
     * // Paint the background gray and determine whether to draw a circle.
     * function repaint() {
     *   background(200);
     *   if (checkbox.checked() === true) {
     *     circle(50, 50, 30);
     *   }
     * }
     */
    changed(fxn) {
      Element._adjustListener('change', fxn, this);
      return this;
    }

    /**
     * Calls a function when the element receives input.
     *
     * `myElement.input()` is often used to with text inputs and sliders. Calling
     * `myElement.input(false)` disables the function.
     *
     * @param  {Function|Boolean} fxn function to call when input is detected within
     *                                the element.
     *                                `false` disables the function.
     * @chainable
     *
     * @example
     * let slider;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a slider and place it beneath the canvas.
     *   slider = createSlider(0, 255, 200);
     *   slider.position(0, 100);
     *
     *   // Call repaint() when the slider changes.
     *   slider.input(repaint);
     *
     *   describe('A gray square with a range slider underneath it. The background changes shades of gray when the slider is moved.');
     * }
     *
     * // Paint the background using slider's value.
     * function repaint() {
     *   let g = slider.value();
     *   background(g);
     * }
     *
     * @example
     * let input;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an input and place it beneath the canvas.
     *   input = createInput('');
     *   input.position(0, 100);
     *
     *   // Call repaint() when input is detected.
     *   input.input(repaint);
     *
     *   describe('A gray square with a text input bar beneath it. Any text written in the input appears in the middle of the square.');
     * }
     *
     * // Paint the background gray and display the input's value.
     * function repaint() {
     *   background(200);
     *   let msg = input.value();
     *   text(msg, 5, 50);
     * }
     */
    input(fxn) {
      Element._adjustListener('input', fxn, this);
      return this;
    }

    /**
     * Calls a function when the user drops a file on the element.
     *
     * The first parameter, `callback`, is a function to call once the file loads.
     * The callback function should have one parameter, `file`, that's a
     * <a href="#/p5.File">p5.File</a> object. If the user drops multiple files on
     * the element, `callback`, is called once for each file.
     *
     * The second parameter, `fxn`, is a function to call when the browser detects
     * one or more dropped files. The callback function should have one
     * parameter, `event`, that's a
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/DragEvent">DragEvent</a>.
     *
     * @param  {Function} callback  called when a file loads. Called once for each file dropped.
     * @param  {Function} [fxn]     called once when any files are dropped.
     * @chainable
     *
     * @example
     * // Drop an image on the canvas to view
     * // this example.
     * let img;
     *
     * function setup() {
     *   let c = createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Call handleFile() when a file that's dropped on the canvas has loaded.
     *   c.drop(handleFile);
     *
     *   describe('A gray square. When the user drops an image on the square, it is displayed.');
     * }
     *
     * // Remove the existing image and display the new one.
     * function handleFile(file) {
     *   // Remove the current image, if any.
     *   if (img) {
     *     img.remove();
     *   }
     *
     *   // Create an <img> element with the
     *   // dropped file.
     *   img = createImg(file.data, '');
     *   img.hide();
     *
     *   // Draw the image.
     *   image(img, 0, 0, width, height);
     * }
     *
     * @example
     * // Drop an image on the canvas to view
     * // this example.
     * let img;
     * let msg;
     *
     * function setup() {
     *   let c = createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Call functions when the user drops a file on the canvas
     *   // and when the file loads.
     *   c.drop(handleFile, handleDrop);
     *
     *   describe('A gray square. When the user drops an image on the square, it is displayed. The id attribute of canvas element is also displayed.');
     * }
     *
     * // Display the image when it loads.
     * function handleFile(file) {
     *   // Remove the current image, if any.
     *   if (img) {
     *     img.remove();
     *   }
     *
     *   // Create an img element with the dropped file.
     *   img = createImg(file.data, '');
     *   img.hide();
     *
     *   // Draw the image.
     *   image(img, 0, 0, width, height);
     * }
     *
     * // Display the file's name when it loads.
     * function handleDrop(event) {
     *   // Remove current paragraph, if any.
     *   if (msg) {
     *     msg.remove();
     *   }
     *
     *   // Use event to get the drop target's id.
     *   let id = event.target.id;
     *
     *   // Write the canvas' id beneath it.
     *   msg = createP(id);
     *   msg.position(0, 100);
     *
     *   // Set the font color randomly for each drop.
     *   let c = random(['red', 'green', 'blue']);
     *   msg.style('color', c);
     *   msg.style('font-size', '12px');
     * }
     */
    drop(callback, fxn) {
      // Is the file stuff supported?
      if (window.File && window.FileReader && window.FileList && window.Blob) {
        if (!this._dragDisabled) {
          this._dragDisabled = true;

          const preventDefault = function (evt) {
            evt.preventDefault();
          };

          // If you want to be able to drop you've got to turn off
          // a lot of default behavior.
          // avoid `attachListener` here, since it overrides other handlers.
          this.elt.addEventListener('dragover', preventDefault);

          // If this is a drag area we need to turn off the default behavior
          this.elt.addEventListener('dragleave', preventDefault);
        }

        // Deal with the files
        Element._attachListener(
          'drop',
          function (evt) {
            evt.preventDefault();
            // Call the second argument as a callback that receives the raw drop event
            if (typeof fxn === 'function') {
              fxn.call(this, evt);
            }
            // A FileList
            const files = evt.dataTransfer.files;

            // Load each one and trigger the callback
            for (const f of files) {
              File._load(f, callback);
            }
          },
          this
        );
      } else {
        console.log('The File APIs are not fully supported in this browser.');
      }

      return this;
    }

    /**
     * Makes the element draggable.
     *
     * The parameter, `elmnt`, is optional. If another
     * <a href="#/p5.Element">p5.Element</a> object is passed, as in
     * `myElement.draggable(otherElement)`, the other element will become draggable.
     *
     * @param  {p5.Element} [elmnt]  another <a href="#/p5.Element">p5.Element</a>.
     * @chainable
     *
     * @example
     * let stickyNote;
     * let textInput;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a div element and style it.
     *   stickyNote = createDiv('Note');
     *   stickyNote.position(5, 5);
     *   stickyNote.size(80, 20);
     *   stickyNote.style('font-size', '16px');
     *   stickyNote.style('font-family', 'Comic Sans MS');
     *   stickyNote.style('background', 'orchid');
     *   stickyNote.style('padding', '5px');
     *
     *   // Make the note draggable.
     *   stickyNote.draggable();
     *
     *   // Create a panel div and style it.
     *   let panel = createDiv('');
     *   panel.position(5, 40);
     *   panel.size(80, 50);
     *   panel.style('background', 'orchid');
     *   panel.style('font-size', '16px');
     *   panel.style('padding', '5px');
     *   panel.style('text-align', 'center');
     *
     *   // Make the panel draggable.
     *   panel.draggable();
     *
     *   // Create a text input and style it.
     *   textInput = createInput('Note');
     *   textInput.size(70);
     *
     *   // Add the input to the panel.
     *   textInput.parent(panel);
     *
     *   // Call handleInput() when text is input.
     *   textInput.input(handleInput);
     *
     *   describe(
     *     'A gray square with two purple rectangles that move when dragged. The top rectangle displays the text that is typed into the bottom rectangle.'
     *   );
     * }
     *
     * // Update stickyNote's HTML when text is input.
     * function handleInput() {
     *   stickyNote.html(textInput.value());
     * }
     */
    draggable(elmMove) {
      let isTouch = 'ontouchstart' in window;

      let x = 0,
        y = 0,
        px = 0,
        py = 0,
        elmDrag,
        dragMouseDownEvt = isTouch ? 'touchstart' : 'mousedown',
        closeDragElementEvt = isTouch ? 'touchend' : 'mouseup',
        elementDragEvt = isTouch ? 'touchmove' : 'mousemove';

      if (elmMove === undefined) {
        elmMove = this.elt;
        elmDrag = elmMove;
      } else if (elmMove !== this.elt && elmMove.elt !== this.elt) {
        elmMove = elmMove.elt;
        elmDrag = this.elt;
      }

      elmDrag.addEventListener(dragMouseDownEvt, dragMouseDown, false);
      elmDrag.style.cursor = 'move';

      function dragMouseDown(e) {
        e = e || window.event;

        if (isTouch) {
          const touches = e.changedTouches;
          px = parseInt(touches[0].clientX);
          py = parseInt(touches[0].clientY);
        } else {
          px = parseInt(e.clientX);
          py = parseInt(e.clientY);
        }

        document.addEventListener(closeDragElementEvt, closeDragElement, false);
        document.addEventListener(elementDragEvt, elementDrag, false);
        return false;
      }

      function elementDrag(e) {
        e = e || window.event;

        if (isTouch) {
          const touches = e.changedTouches;
          x = px - parseInt(touches[0].clientX);
          y = py - parseInt(touches[0].clientY);
          px = parseInt(touches[0].clientX);
          py = parseInt(touches[0].clientY);
        } else {
          x = px - parseInt(e.clientX);
          y = py - parseInt(e.clientY);
          px = parseInt(e.clientX);
          py = parseInt(e.clientY);
        }

        elmMove.style.left = elmMove.offsetLeft - x + 'px';
        elmMove.style.top = elmMove.offsetTop - y + 'px';
      }

      function closeDragElement() {
        document.removeEventListener(
          closeDragElementEvt,
          closeDragElement,
          false
        );
        document.removeEventListener(elementDragEvt, elementDrag, false);
      }

      return this;
    }

    /**
     *
     * @private
     * @static
     * @param {String} ev
     * @param {Boolean|Function} fxn
     * @param {Element} ctx
     * @chainable
     * @alt
     * General handler for event attaching and detaching
     */
    static _adjustListener(ev, fxn, ctx) {
      if (fxn === false) {
        Element._detachListener(ev, ctx);
      } else {
        Element._attachListener(ev, fxn, ctx);
      }
      return this;
    }

    /**
     *
     * @private
     * @static
     * @param {String} ev
     * @param {Function} fxn
     * @param {Element} ctx
     */
    static _attachListener(ev, fxn, ctx) {
      // detach the old listener if there was one
      if (ctx._events[ev]) {
        Element._detachListener(ev, ctx);
      }
      const f = fxn.bind(ctx);
      ctx.elt.addEventListener(ev, f, {
        capture: false,
        signal: ctx._pInst._removeSignal
      });
      ctx._events[ev] = f;
    }

    /**
     *
     * @private
     * @static
     * @param {String} ev
     * @param {Element} ctx
     */
    static _detachListener(ev, ctx) {
      const f = ctx._events[ev];
      ctx.elt.removeEventListener(ev, f, false);
      ctx._events[ev] = null;
    }
  }
  function element(p5, fn){
    /**
     * A class to describe an
     * <a href="https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML/Getting_started" target="_blank">HTML element</a>.
     *
     * Sketches can use many elements. Common elements include the drawing canvas,
     * buttons, sliders, webcam feeds, and so on.
     *
     * All elements share the methods of the `p5.Element` class. They're created
     * with functions such as <a href="#/p5/createCanvas">createCanvas()</a> and
     * <a href="#/p5/createButton">createButton()</a>.
     *
     * @class p5.Element
     * @param {HTMLElement} elt wrapped DOM element.
     * @param {p5} [pInst] pointer to p5 instance.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a button element and
     *   // place it beneath the canvas.
     *   let btn = createButton('change');
     *   btn.position(0, 100);
     *
     *   // Call randomColor() when
     *   // the button is pressed.
     *   btn.mousePressed(randomColor);
     *
     *   describe('A gray square with a button that says "change" beneath it. The square changes color when the user presses the button.');
     * }
     *
     * // Paint the background either
     * // red, yellow, blue, or green.
     * function randomColor() {
     *   let c = random(['red', 'yellow', 'blue', 'green']);
     *   background(c);
     * }
     */
    p5.Element = Element;

    /**
     * A `Number` property that stores the element's width.
     *
     * @type {Number}
     * @property width
     * @for p5.Element
     */

    /**
     * A `Number` property that stores the element's height.
     *
     * @type {Number}
     * @property height
     * @for p5.Element
     */

    /**
     * The element's underlying `HTMLElement` object.
     *
     * The
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement" target="_blank">HTMLElement</a>
     * object's properties and methods can be used directly.
     *
     * @example
     * function setup() {
     *   // Create a canvas element and
     *   // assign it to cnv.
     *   let cnv = createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the border style for the
     *   // canvas.
     *   cnv.elt.style.border = '5px dashed deeppink';
     *
     *   describe('A gray square with a pink border drawn with dashed lines.');
     * }
     *
     * @type {HTMLElement}
     * @property elt
     * @for p5.Element
     * @name elt
     * @readOnly
     */
  }

  if(typeof p5 !== 'undefined'){
    element(p5);
  }

  /*
   * This module defines the filters for use with image buffers.
   *
   * This module is basically a collection of functions stored in an object
   * as opposed to modules. The functions are destructive, modifying
   * the passed in canvas rather than creating a copy.
   *
   * Generally speaking users of this module will use the Filters.apply method
   * on a canvas to create an effect.
   *
   * A number of functions are borrowed/adapted from
   * http://www.html5rocks.com/en/tutorials/canvas/imagefilters/
   * or the java processing implementation.
   *
   * @private
   */

  const Filters = {

    /*
     * Helper functions
     */

    /**
     * Returns the pixel buffer for a canvas.
     *
     * @private
     *
     * @param  {Canvas|ImageData} canvas the canvas to get pixels from
     * @return {Uint8ClampedArray}       a one-dimensional array containing
     *                                   the data in the RGBA order, with integer
     *                                   values between 0 and 255.
     */
    _toPixels(canvas) {
      // Return pixel data if 'canvas' is an ImageData object.
      if (canvas instanceof ImageData) {
        return canvas.data;
      } else {
        // Check 2D context support.
        if (canvas.getContext('2d')) {
          // Retrieve pixel data.
          return canvas
            .getContext('2d')
            .getImageData(0, 0, canvas.width, canvas.height).data;
        } else if (canvas.getContext('webgl')) { //Check WebGL context support
          const gl = canvas.getContext('webgl');
          // Calculate the size of pixel data
          // (4 bytes per pixel - one byte for each RGBA channel).
          const len = gl.drawingBufferWidth * gl.drawingBufferHeight * 4;
          const data = new Uint8Array(len);
          // Use gl.readPixels to fetch pixel data from the WebGL
          // canvas, storing it in the data array as UNSIGNED_BYTE integers.
          gl.readPixels(
            0,
            0,
            canvas.width,
            canvas.height,
            gl.RGBA,
            gl.UNSIGNED_BYTE,
            data
          );
          return data;
        }
      }
    },

    /**
     * Returns a 32-bit number containing ARGB data at the ith pixel in the
     * 1D array containing pixels data.
     *
     * @private
     *
     * @param  {Uint8ClampedArray} data array returned by _toPixels()
     * @param  {Integer}           i    index of a 1D Image Array
     * @return {Integer}                32-bit integer value representing
     *                                  ARGB value.
     */
    _getARGB(data, i) {
      // Determine the starting position in the 'data' array for the 'i'-th pixel.
      const offset = i * 4;
      return (
        // Combining the extracted components using bitwise OR operations to form the final ARGB value.
        ((data[offset + 3] << 24) & 0xff000000) | //Extract alpha component
        ((data[offset] << 16) & 0x00ff0000) | //Extract Red component
        ((data[offset + 1] << 8) & 0x0000ff00) | //Extract green component
        (data[offset + 2] & 0x000000ff) //Extract blue component
      );
    },

    /**
     * Modifies pixels RGBA values to values contained in the data object.
     *
     * @private
     *
     * @param {Uint8ClampedArray} pixels array returned by _toPixels()
     * @param {Int32Array}        data   source 1D array where each value
     *                                   represents ARGB values
     */
    _setPixels(pixels, data) {
      let offset = 0;
      for (let i = 0, al = pixels.length; i < al; i++) {
        offset = i * 4;
        pixels[offset + 0] = (data[i] & 0x00ff0000) >>> 16;
        pixels[offset + 1] = (data[i] & 0x0000ff00) >>> 8;
        pixels[offset + 2] = data[i] & 0x000000ff;
        pixels[offset + 3] = (data[i] & 0xff000000) >>> 24;
      }
    },


    /**
     * Returns the ImageData object for a canvas.
     * https://developer.mozilla.org/en-US/docs/Web/API/ImageData
     *
     * @private
     *
     * @param  {Canvas|ImageData} canvas canvas to get image data from
     * @return {ImageData}               Holder of pixel data (and width and
     *                                   height) for a canvas
     */
    _toImageData(canvas) {
      if (canvas instanceof ImageData) {
        return canvas;
      } else {
        return canvas
          .getContext('2d')
          .getImageData(0, 0, canvas.width, canvas.height);
      }
    },


    /**
     * Returns a blank ImageData object.
     *
     * @private
     *
     * @param  {Integer} width
     * @param  {Integer} height
     * @return {ImageData}
     */
    _createImageData(width, height) {
      Filters._tmpCanvas = document.createElement('canvas');
      Filters._tmpCtx = Filters._tmpCanvas.getContext('2d');
      return this._tmpCtx.createImageData(width, height);
    },

    /**
     * Applys a filter function to a canvas.
     *
     * The difference between this and the actual filter functions defined below
     * is that the filter functions generally modify the pixel buffer but do
     * not actually put that data back to the canvas (where it would actually
     * update what is visible). By contrast this method does make the changes
     * actually visible in the canvas.
     *
     * The apply method is the method that callers of this module would generally
     * use. It has been separated from the actual filters to support an advanced
     * use case of creating a filter chain that executes without actually updating
     * the canvas in between everystep.
     *
     * @private
     * @param  {HTMLCanvasElement} canvas The input canvas to apply the filter on.
     * @param  {function(ImageData,Object)} func The filter function to apply to the canvas's pixel data.
     * @param  {Object} filterParam An optional parameter to pass to the filter function.
     */
    apply(canvas, func, filterParam) {
      const pixelsState = canvas.getContext('2d');
      const imageData = pixelsState.getImageData(
        0, 0, canvas.width, canvas.height);

      //Filters can either return a new ImageData object, or just modify
      //the one they received.
      const newImageData = func(imageData, filterParam);
      //If new ImageData is returned, replace the canvas's pixel data with it.
      if (newImageData instanceof ImageData) {
        pixelsState.putImageData(
          newImageData,
          0,
          0,
          0,
          0,
          canvas.width,
          canvas.height
        );
      } else {  //Restore the original pixel.
        pixelsState.putImageData(
          imageData,
          0,
          0,
          0,
          0,
          canvas.width,
          canvas.height
        );
      }
    },

    /*
     * Filters
     */

    /**
     * Converts the image to black and white pixels depending if they are above or
     * below the threshold defined by the level parameter. The parameter must be
     * between 0.0 (black) and 1.0 (white). If no level is specified, 0.5 is used.
     *
     * Borrowed from http://www.html5rocks.com/en/tutorials/canvas/imagefilters/
     *
     * @private
     * @param  {Canvas} canvas Canvas to apply thershold filter on.
     * @param  {Float} level Threshold level (0-1).
     */
    threshold(canvas, level = 0.5) {
      const pixels = Filters._toPixels(canvas);

      // Calculate threshold value on a (0-255) scale.
      const thresh = Math.floor(level * 255);

      for (let i = 0; i < pixels.length; i += 4) {
        const r = pixels[i];
        const g = pixels[i + 1];
        const b = pixels[i + 2];
        // CIE luminance for RGB
        const gray = 0.2126 * r + 0.7152 * g + 0.0722 * b;
        let val;
        if (gray >= thresh) {
          val = 255;
        } else {
          val = 0;
        }
        pixels[i] = pixels[i + 1] = pixels[i + 2] = val; //set pixel to val.
      }
    },

    /**
     * Converts any colors in the image to grayscale equivalents.
     * No parameter is used.
     *
     * Borrowed from http://www.html5rocks.com/en/tutorials/canvas/imagefilters/
     *
     * @private
     * @param {Canvas} canvas Canvas to apply gray filter on.
     */
    gray(canvas) {
      const pixels = Filters._toPixels(canvas);

      for (let i = 0; i < pixels.length; i += 4) {
        const r = pixels[i];
        const g = pixels[i + 1];
        const b = pixels[i + 2];

        // CIE luminance for RGB
        const gray = 0.2126 * r + 0.7152 * g + 0.0722 * b;
        pixels[i] = pixels[i + 1] = pixels[i + 2] = gray; // set pixel to gray.
      }
    },

    /**
     * Sets the alpha channel to entirely opaque. No parameter is used.
     *
     * @private
     * @param {Canvas} canvas
     */
    opaque(canvas) {
      const pixels = Filters._toPixels(canvas);

      for (let i = 0; i < pixels.length; i += 4) {
        pixels[i + 3] = 255;
      }

      return pixels;
    },

    /**
     * Sets each pixel to its inverse value. No parameter is used.
     * @private
     * @param  {Canvas} canvas
     */
    invert(canvas) {
      const pixels = Filters._toPixels(canvas);

      for (let i = 0; i < pixels.length; i += 4) {
        pixels[i] = 255 - pixels[i];
        pixels[i + 1] = 255 - pixels[i + 1];
        pixels[i + 2] = 255 - pixels[i + 2];
      }
    },

    /**
     * Limits each channel of the image to the number of colors specified as
     * the parameter. The parameter can be set to values between 2 and 255, but
     * results are most noticeable in the lower ranges.
     *
     * Adapted from java based processing implementation
     *
     * @private
     * @param  {Canvas} canvas
     * @param  {Integer} level
     */
    posterize(canvas, level = 4) {
      const pixels = Filters._toPixels(canvas);
      if (level < 2 || level > 255) {
        throw new Error(
          'Level must be greater than 2 and less than 255 for posterize'
        );
      }

      const levels1 = level - 1;
      for (let i = 0; i < pixels.length; i += 4) {
        const rlevel = pixels[i];
        const glevel = pixels[i + 1];
        const blevel = pixels[i + 2];

        // New pixel value by posterizing each color.
        pixels[i] = ((rlevel * level) >> 8) * 255 / levels1;
        pixels[i + 1] = ((glevel * level) >> 8) * 255 / levels1;
        pixels[i + 2] = ((blevel * level) >> 8) * 255 / levels1;
      }
    },

    /**
     * Increases the bright areas in an image.
     * @private
     * @param  {Canvas} canvas
     */
    dilate(canvas) {
      const pixels = Filters._toPixels(canvas);
      let currIdx = 0;
      const maxIdx = pixels.length ? pixels.length / 4 : 0;
      const out = new Int32Array(maxIdx);
      let currRowIdx, maxRowIdx, colOrig, colOut, currLum;

      let idxRight, idxLeft, idxUp, idxDown;
      let colRight, colLeft, colUp, colDown;
      let lumRight, lumLeft, lumUp, lumDown;
      // Iterates through rows of pixels.
      while (currIdx < maxIdx) {
        currRowIdx = currIdx;
        maxRowIdx = currIdx + canvas.width;
        // Iterates through pixels within the current row.
        while (currIdx < maxRowIdx) {
          // Get original color of current pixel.
          colOrig = colOut = Filters._getARGB(pixels, currIdx);
          idxLeft = currIdx - 1;
          idxRight = currIdx + 1;
          idxUp = currIdx - canvas.width;
          idxDown = currIdx + canvas.width;

          // Adjust the indices to avoid going out of bounds.
          if (idxLeft < currRowIdx) {
            idxLeft = currIdx;
          }
          if (idxRight >= maxRowIdx) {
            idxRight = currIdx;
          }
          if (idxUp < 0) {
            idxUp = 0;
          }
          if (idxDown >= maxIdx) {
            idxDown = currIdx;
          }
          colUp = Filters._getARGB(pixels, idxUp);
          colLeft = Filters._getARGB(pixels, idxLeft);
          colDown = Filters._getARGB(pixels, idxDown);
          colRight = Filters._getARGB(pixels, idxRight);

          // Compute luminance
          currLum =
            77 * ((colOrig >> 16) & 0xff) +
            151 * ((colOrig >> 8) & 0xff) +
            28 * (colOrig & 0xff);
          lumLeft =
            77 * ((colLeft >> 16) & 0xff) +
            151 * ((colLeft >> 8) & 0xff) +
            28 * (colLeft & 0xff);
          lumRight =
            77 * ((colRight >> 16) & 0xff) +
            151 * ((colRight >> 8) & 0xff) +
            28 * (colRight & 0xff);
          lumUp =
            77 * ((colUp >> 16) & 0xff) +
            151 * ((colUp >> 8) & 0xff) +
            28 * (colUp & 0xff);
          lumDown =
            77 * ((colDown >> 16) & 0xff) +
            151 * ((colDown >> 8) & 0xff) +
            28 * (colDown & 0xff);

          // Update the output color based on the highest luminance value
          if (lumLeft > currLum) {
            colOut = colLeft;
            currLum = lumLeft;
          }
          if (lumRight > currLum) {
            colOut = colRight;
            currLum = lumRight;
          }
          if (lumUp > currLum) {
            colOut = colUp;
            currLum = lumUp;
          }
          if (lumDown > currLum) {
            colOut = colDown;
            currLum = lumDown;
          }
          // Store the updated color.
          out[currIdx++] = colOut;
        }
      }
      Filters._setPixels(pixels, out);
    },

    /**
     * Reduces the bright areas in an image.
     * Similar to `dilate()`, but updates the output color based on the lowest luminance value.
     * @private
     * @param  {Canvas} canvas
     */
    erode(canvas) {
      const pixels = Filters._toPixels(canvas);
      let currIdx = 0;
      const maxIdx = pixels.length ? pixels.length / 4 : 0;
      const out = new Int32Array(maxIdx);
      let currRowIdx, maxRowIdx, colOrig, colOut, currLum;
      let idxRight, idxLeft, idxUp, idxDown;
      let colRight, colLeft, colUp, colDown;
      let lumRight, lumLeft, lumUp, lumDown;

      while (currIdx < maxIdx) {
        currRowIdx = currIdx;
        maxRowIdx = currIdx + canvas.width;
        while (currIdx < maxRowIdx) {
          colOrig = colOut = Filters._getARGB(pixels, currIdx);
          idxLeft = currIdx - 1;
          idxRight = currIdx + 1;
          idxUp = currIdx - canvas.width;
          idxDown = currIdx + canvas.width;

          if (idxLeft < currRowIdx) {
            idxLeft = currIdx;
          }
          if (idxRight >= maxRowIdx) {
            idxRight = currIdx;
          }
          if (idxUp < 0) {
            idxUp = 0;
          }
          if (idxDown >= maxIdx) {
            idxDown = currIdx;
          }
          colUp = Filters._getARGB(pixels, idxUp);
          colLeft = Filters._getARGB(pixels, idxLeft);
          colDown = Filters._getARGB(pixels, idxDown);
          colRight = Filters._getARGB(pixels, idxRight);

          //compute luminance
          currLum =
            77 * ((colOrig >> 16) & 0xff) +
            151 * ((colOrig >> 8) & 0xff) +
            28 * (colOrig & 0xff);
          lumLeft =
            77 * ((colLeft >> 16) & 0xff) +
            151 * ((colLeft >> 8) & 0xff) +
            28 * (colLeft & 0xff);
          lumRight =
            77 * ((colRight >> 16) & 0xff) +
            151 * ((colRight >> 8) & 0xff) +
            28 * (colRight & 0xff);
          lumUp =
            77 * ((colUp >> 16) & 0xff) +
            151 * ((colUp >> 8) & 0xff) +
            28 * (colUp & 0xff);
          lumDown =
            77 * ((colDown >> 16) & 0xff) +
            151 * ((colDown >> 8) & 0xff) +
            28 * (colDown & 0xff);

          if (lumLeft < currLum) {
            colOut = colLeft;
            currLum = lumLeft;
          }
          if (lumRight < currLum) {
            colOut = colRight;
            currLum = lumRight;
          }
          if (lumUp < currLum) {
            colOut = colUp;
            currLum = lumUp;
          }
          if (lumDown < currLum) {
            colOut = colDown;
            currLum = lumDown;
          }
          // Store the updated color.
          out[currIdx++] = colOut;
        }
      }
      Filters._setPixels(pixels, out);
    },

    blur(canvas, radius) {
      blurARGB(canvas, radius);
    }
  };

  // BLUR

  // Internal kernel stuff for the gaussian blur filter.
  let blurRadius;
  let blurKernelSize;
  let blurKernel;
  let blurMult;

  /*
   * Port of https://github.com/processing/processing/blob/
   * main/core/src/processing/core/PImage.java#L1250
   *
   * Optimized code for building the blur kernel.
   * further optimized blur code (approx. 15% for radius=20)
   * bigger speed gains for larger radii (~30%)
   * added support for various image types (ALPHA, RGB, ARGB)
   * [toxi 050728]
   */
  function buildBlurKernel(r) {
    let radius = (r * 3.5) | 0;
    radius = radius < 1 ? 1 : radius < 248 ? radius : 248;

    if (blurRadius !== radius) {
      blurRadius = radius;
      // Calculating the size of the blur kernel
      blurKernelSize = (1 + blurRadius) << 1;
      blurKernel = new Int32Array(blurKernelSize);
      blurMult = new Array(blurKernelSize);
      for (let l = 0; l < blurKernelSize; l++) {
        blurMult[l] = new Int32Array(256);
      }

      let bk, bki;
      let bm, bmi;
      // Generating blur kernel values.
      for (let i = 1, radiusi = radius - 1; i < radius; i++) {
        blurKernel[radius + i] = blurKernel[radiusi] = bki = radiusi * radiusi;
        bm = blurMult[radius + i];
        bmi = blurMult[radiusi--];
        for (let j = 0; j < 256; j++) {
          bm[j] = bmi[j] = bki * j;
        }
      }
      bk = blurKernel[radius] = radius * radius;
      bm = blurMult[radius];

      for (let k = 0; k < 256; k++) {
        bm[k] = bk * k;
      }
    }
  }

  // Port of https://github.com/processing/processing/blob/
  // main/core/src/processing/core/PImage.java#L1433
  function blurARGB(canvas, radius) {
    // Get pixel data.
    const pixels = Filters._toPixels(canvas);
    const width = canvas.width;
    const height = canvas.height;
    const numPackedPixels = width * height;
    const argb = new Int32Array(numPackedPixels);
    for (let j = 0; j < numPackedPixels; j++) {
      argb[j] = Filters._getARGB(pixels, j);
    }
    let sum, cr, cg, cb, ca;
    let read, ri, ym, ymi, bk0;
    const a2 = new Int32Array(numPackedPixels);
    const r2 = new Int32Array(numPackedPixels);
    const g2 = new Int32Array(numPackedPixels);
    const b2 = new Int32Array(numPackedPixels);
    let yi = 0;
    buildBlurKernel(radius);
    let x, y, i;
    let bm;
    // Horizontal pass.
    for (y = 0; y < height; y++) {
      for (x = 0; x < width; x++) {
        cb = cg = cr = ca = sum = 0;
        read = x - blurRadius;
        // Handle edge cases.
        if (read < 0) {
          bk0 = -read;
          read = 0;
        } else {
          if (read >= width) {
            break;
          }
          bk0 = 0;
        }
        for (i = bk0; i < blurKernelSize; i++) {
          if (read >= width) {
            break;
          }
          const c = argb[read + yi];
          bm = blurMult[i];
          ca += bm[(c & -16777216) >>> 24];
          cr += bm[(c & 16711680) >> 16];
          cg += bm[(c & 65280) >> 8];
          cb += bm[c & 255];
          sum += blurKernel[i];
          read++;
        }
        ri = yi + x;
        a2[ri] = ca / sum;
        r2[ri] = cr / sum;
        g2[ri] = cg / sum;
        b2[ri] = cb / sum;
      }
      yi += width;
    }
    yi = 0;
    ym = -blurRadius;
    ymi = ym * width;
    //  Vertical pass.
    for (y = 0; y < height; y++) {
      for (x = 0; x < width; x++) {
        cb = cg = cr = ca = sum = 0;
        // Handle edge cases.
        if (ym < 0) {
          bk0 = ri = -ym;
          read = x;
        } else {
          if (ym >= height) {
            break;
          }
          bk0 = 0;
          ri = ym;
          read = x + ymi;
        }
        for (i = bk0; i < blurKernelSize; i++) {
          if (ri >= height) {
            break;
          }
          bm = blurMult[i];
          ca += bm[a2[read]];
          cr += bm[r2[read]];
          cg += bm[g2[read]];
          cb += bm[b2[read]];
          sum += blurKernel[i];
          ri++;
          read += width;
        }
        // Set final ARGB value
        argb[x + yi] =
          ((ca / sum) << 24) |
          ((cr / sum) << 16) |
          ((cg / sum) << 8) |
          (cb / sum);
      }
      yi += width;
      ymi += width;
      ym++;
    }
    Filters._setPixels(pixels, argb);
  }

  function downloadFile(data, fName, extension) {
    const fx = _checkFileExtension(fName, extension);
    const filename = fx[0];
    let saveData = data;

    if (!(saveData instanceof Blob)) {
      saveData = new Blob([data]);
    }

    if(document){
      const url = URL.createObjectURL(saveData);
      const link = document.createElement('a');
      link.href = url;
      link.download = filename;
      link.click();
      URL.revokeObjectURL(url);
    }
  }

  function _checkFileExtension(filename, extension) {
    if (!extension || extension === true || extension === 'true') {
      extension = '';
    }
    if (!filename) {
      filename = 'untitled';
    }
    let ext = '';
    // make sure the file will have a name, see if filename needs extension
    if (filename && filename.includes('.')) {
      ext = filename.split('.').pop();
    }
    // append extension if it doesn't exist
    if (extension) {
      if (ext !== extension) {
        ext = extension;
        filename = `${filename}.${ext}`;
      }
    }
    return [filename, ext];
  }

  /**
   * @module Image
   * @submodule Image
   * @requires core
   * @requires constants
   * @requires filters
   */


  class Image {
    constructor(width, height) {
      this.width = width;
      this.height = height;
      this.canvas = document.createElement('canvas');
      this.canvas.width = this.width;
      this.canvas.height = this.height;
      this.drawingContext = this.canvas.getContext('2d');
      this._pixelsState = this;
      this._pixelDensity = 1;
      //Object for working with GIFs, defaults to null
      this.gifProperties = null;
      //For WebGL Texturing only: used to determine whether to reupload texture to GPU
      this._modified = false;
      this.pixels = [];
    }

    /**
     * Gets or sets the pixel density for high pixel density displays.
     *
     * By default, the density will be set to 1.
     *
     * Call this method with no arguments to get the default density, or pass
     * in a number to set the density. If a non-positive number is provided,
     * it defaults to 1.
     *
     * @param {Number} [density] A scaling factor for the number of pixels per
     * side
     * @returns {Number} The current density if called without arguments, or the instance for chaining if setting density.
     */
    pixelDensity(density) {
      if (typeof density !== 'undefined') {
      // Setter: set the density and handle resize
        if (density <= 0) {

          // p5._friendlyParamError(errorObj, 'pixelDensity');

          // Default to 1 in case of an invalid value
          density = 1;
        }

        this._pixelDensity = density;

        // Adjust canvas dimensions based on pixel density
        this.width /= density;
        this.height /= density;

        return this; // Return the image instance for chaining if needed
      } else {
      // Getter: return the default density
        return this._pixelDensity;
      }
    }

    /**
     * Helper function for animating GIF-based images with time
     */
    _animateGif(pInst) {
      const props = this.gifProperties;
      const curTime = pInst._lastRealFrameTime || window.performance.now();
      if (props.lastChangeTime === 0) {
        props.lastChangeTime = curTime;
      }
      if (props.playing) {
        props.timeDisplayed = curTime - props.lastChangeTime;
        const curDelay = props.frames[props.displayIndex].delay;
        if (props.timeDisplayed >= curDelay) {
          //GIF is bound to 'realtime' so can skip frames
          const skips = Math.floor(props.timeDisplayed / curDelay);
          props.timeDisplayed = 0;
          props.lastChangeTime = curTime;
          props.displayIndex += skips;
          props.loopCount = Math.floor(props.displayIndex / props.numFrames);
          if (props.loopLimit !== null && props.loopCount >= props.loopLimit) {
            props.playing = false;
          } else {
            const ind = props.displayIndex % props.numFrames;
            this.drawingContext.putImageData(props.frames[ind].image, 0, 0);
            props.displayIndex = ind;
            this.setModified(true);
          }
        }
      }
    }

    /**
     * Loads the current value of each pixel in the image into the `img.pixels`
     * array.
     *
     * `img.loadPixels()` must be called before reading or modifying pixel
     * values.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Image object.
     *   let img = createImage(66, 66);
     *
     *   // Load the image's pixels.
     *   img.loadPixels();
     *
     *   // Set the pixels to black.
     *   for (let x = 0; x < img.width; x += 1) {
     *     for (let y = 0; y < img.height; y += 1) {
     *       img.set(x, y, 0);
     *     }
     *   }
     *
     *   // Update the image.
     *   img.updatePixels();
     *
     *   // Display the image.
     *   image(img, 17, 17);
     *
     *   describe('A black square drawn in the middle of a gray square.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Image object.
     *   let img = createImage(66, 66);
     *
     *   // Load the image's pixels.
     *   img.loadPixels();
     *
     *   for (let i = 0; i < img.pixels.length; i += 4) {
     *     // Red.
     *     img.pixels[i] = 0;
     *     // Green.
     *     img.pixels[i + 1] = 0;
     *     // Blue.
     *     img.pixels[i + 2] = 0;
     *     // Alpha.
     *     img.pixels[i + 3] = 255;
     *   }
     *
     *   // Update the image.
     *   img.updatePixels();
     *
     *   // Display the image.
     *   image(img, 17, 17);
     *
     *   describe('A black square drawn in the middle of a gray square.');
     * }
     */
    loadPixels() {
      // Renderer2D.prototype.loadPixels.call(this);
      const pixelsState = this._pixelsState;
      const pd = this._pixelDensity;
      const w = this.width * pd;
      const h = this.height * pd;
      const imageData = this.drawingContext.getImageData(0, 0, w, h);
      // @todo this should actually set pixels per object, so diff buffers can
      // have diff pixel arrays.
      pixelsState.imageData = imageData;
      this.pixels = pixelsState.pixels = imageData.data;
      this.setModified(true);
    }

    /**
     * Updates the canvas with the RGBA values in the
     * <a href="#/p5.Image/pixels">img.pixels</a> array.
     *
     * `img.updatePixels()` only needs to be called after changing values in
     * the <a href="#/p5.Image/pixels">img.pixels</a> array. Such changes can be
     * made directly after calling
     * <a href="#/p5.Image/loadPixels">img.loadPixels()</a> or by calling
     * <a href="#/p5.Image/set">img.set()</a>.
     *
     * The optional parameters `x`, `y`, `width`, and `height` define a
     * subsection of the image to update. Doing so can improve performance in
     * some cases.
     *
     * If the image was loaded from a GIF, then calling `img.updatePixels()`
     * will update the pixels in current frame.
     *
     * @param {Integer} [x] x-coordinate of the upper-left corner
     *                      of the subsection to update.
     * @param {Integer} [y] y-coordinate of the upper-left corner
     *                      of the subsection to update.
     * @param {Integer} [w] width of the subsection to update.
     * @param {Integer} [h] height of the subsection to update.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Image object.
     *   let img = createImage(66, 66);
     *
     *   // Load the image's pixels.
     *   img.loadPixels();
     *
     *   // Set the pixels to black.
     *   for (let x = 0; x < img.width; x += 1) {
     *     for (let y = 0; y < img.height; y += 1) {
     *       img.set(x, y, 0);
     *     }
     *   }
     *
     *   // Update the image.
     *   img.updatePixels();
     *
     *   // Display the image.
     *   image(img, 17, 17);
     *
     *   describe('A black square drawn in the middle of a gray square.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Image object.
     *   let img = createImage(66, 66);
     *
     *   // Load the image's pixels.
     *   img.loadPixels();
     *
     *   // Set the pixels to black.
     *   for (let i = 0; i < img.pixels.length; i += 4) {
     *     // Red.
     *     img.pixels[i] = 0;
     *     // Green.
     *     img.pixels[i + 1] = 0;
     *     // Blue.
     *     img.pixels[i + 2] = 0;
     *     // Alpha.
     *     img.pixels[i + 3] = 255;
     *   }
     *
     *   // Update the image.
     *   img.updatePixels();
     *
     *   // Display the image.
     *   image(img, 17, 17);
     *
     *   describe('A black square drawn in the middle of a gray square.');
     * }
     */
    updatePixels(x, y, w, h) {
      // Renderer2D.prototype.updatePixels.call(this, x, y, w, h);
      const pixelsState = this._pixelsState;
      const pd = this._pixelDensity;
      if (
        x === undefined &&
        y === undefined &&
        w === undefined &&
        h === undefined
      ) {
        x = 0;
        y = 0;
        w = this.width;
        h = this.height;
      }
      x *= pd;
      y *= pd;
      w *= pd;
      h *= pd;

      if (this.gifProperties) {
        this.gifProperties.frames[this.gifProperties.displayIndex].image =
          pixelsState.imageData;
      }

      this.drawingContext.putImageData(pixelsState.imageData, x, y, 0, 0, w, h);
      this.setModified(true);
    }

    /**
     * Gets a pixel or a region of pixels from the image.
     *
     * `img.get()` is easy to use but it's not as fast as
     * <a href="#/p5.Image/pixels">img.pixels</a>. Use
     * <a href="#/p5.Image/pixels">img.pixels</a> to read many pixel values.
     *
     * The version of `img.get()` with no parameters returns the entire image.
     *
     * The version of `img.get()` with two parameters, as in `img.get(10, 20)`,
     * interprets them as coordinates. It returns an array with the
     * `[R, G, B, A]` values of the pixel at the given point.
     *
     * The version of `img.get()` with four parameters, as in
     * `img,get(10, 20, 50, 90)`, interprets them as
     * coordinates and dimensions. The first two parameters are the coordinates
     * of the upper-left corner of the subsection. The last two parameters are
     * the width and height of the subsection. It returns a subsection of the
     * canvas in a new <a href="#/p5.Image">p5.Image</a> object.
     *
     * Use `img.get()` instead of <a href="#/p5/get">get()</a> to work directly
     * with images.
     *
     * @param  {Number}               x x-coordinate of the pixel.
     * @param  {Number}               y y-coordinate of the pixel.
     * @param  {Number}               w width of the subsection to be returned.
     * @param  {Number}               h height of the subsection to be returned.
     * @return {p5.Image}             subsection as a <a href="#/p5.Image">p5.Image</a> object.
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/rockies.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Copy the image.
     *   let img2 = get();
     *
     *   // Display the copied image on the right.
     *   image(img2, 50, 0);
     *
     *   describe('Two identical mountain landscapes shown side-by-side.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/rockies.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Get a pixel's color.
     *   let c = img.get(50, 90);
     *
     *   // Style the square using the pixel's color.
     *   fill(c);
     *   noStroke();
     *
     *   // Draw the square.
     *   square(25, 25, 50);
     *
     *   describe('A mountain landscape with an olive green square in its center.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/rockies.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Copy half of the image.
     *   let img2 = img.get(0, 0, img.width / 2, img.height / 2);
     *
     *   // Display half of the image.
     *   image(img2, 50, 50);
     *
     *   describe('A mountain landscape drawn on top of another mountain landscape.');
     * }
     */
    /**
     * @return {p5.Image}      whole <a href="#/p5.Image">p5.Image</a>
     */
    /**
     * @param  {Number}        x
     * @param  {Number}        y
     * @return {Number[]}      color of the pixel at (x, y) in array format `[R, G, B, A]`.
     */
    get(x, y, w, h) {
      // p5._validateParameters('p5.Image.get', arguments);
      // return Renderer2D.prototype.get.apply(this, arguments);
      const pixelsState = this._pixelsState;
      const pd = this._pixelDensity;
      const canvas = this.canvas;

      if (typeof x === 'undefined' && typeof y === 'undefined') {
      // get()
        x = y = 0;
        w = pixelsState.width;
        h = pixelsState.height;
      } else {
        x *= pd;
        y *= pd;

        if (typeof w === 'undefined' && typeof h === 'undefined') {
        // get(x,y)
          if (x < 0 || y < 0 || x >= canvas.width || y >= canvas.height) {
            return [0, 0, 0, 0];
          }

          return this._getPixel(x, y);
        }
      // get(x,y,w,h)
      }

      const region = new Image(w*pd, h*pd);
      region.pixelDensity(pd);
      region.canvas
        .getContext('2d')
        .drawImage(canvas, x, y, w * pd, h * pd, 0, 0, w*pd, h*pd);

      return region;
    }

    _getPixel(x, y) {
      let imageData, index;
      imageData = this.drawingContext.getImageData(x, y, 1, 1).data;
      index = 0;
      return [
        imageData[index + 0],
        imageData[index + 1],
        imageData[index + 2],
        imageData[index + 3]
      ];
      // return Renderer2D.prototype._getPixel.apply(this, args);
    }

    /**
     * Sets the color of one or more pixels within an image.
     *
     * `img.set()` is easy to use but it's not as fast as
     * <a href="#/p5.Image/pixels">img.pixels</a>. Use
     * <a href="#/p5.Image/pixels">img.pixels</a> to set many pixel values.
     *
     * `img.set()` interprets the first two parameters as x- and y-coordinates. It
     * interprets the last parameter as a grayscale value, a `[R, G, B, A]` pixel
     * array, a <a href="#/p5.Color">p5.Color</a> object, or another
     * <a href="#/p5.Image">p5.Image</a> object.
     *
     * <a href="#/p5.Image/updatePixels">img.updatePixels()</a> must be called
     * after using `img.set()` for changes to appear.
     *
     * @param {Number}              x x-coordinate of the pixel.
     * @param {Number}              y y-coordinate of the pixel.
     * @param {Number|Number[]|Object}   a grayscale value | pixel array |
     *                                   <a href="#/p5.Color">p5.Color</a> object |
     *                                   <a href="#/p5.Image">p5.Image</a> to copy.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Image object.
     *   let img = createImage(100, 100);
     *
     *   // Set four pixels to black.
     *   img.set(30, 20, 0);
     *   img.set(85, 20, 0);
     *   img.set(85, 75, 0);
     *   img.set(30, 75, 0);
     *
     *   // Update the image.
     *   img.updatePixels();
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   describe('Four black dots arranged in a square drawn on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Image object.
     *   let img = createImage(100, 100);
     *
     *   // Create a p5.Color object.
     *   let black = color(0);
     *
     *   // Set four pixels to black.
     *   img.set(30, 20, black);
     *   img.set(85, 20, black);
     *   img.set(85, 75, black);
     *   img.set(30, 75, black);
     *
     *   // Update the image.
     *   img.updatePixels();
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   describe('Four black dots arranged in a square drawn on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Image object.
     *   let img = createImage(66, 66);
     *
     *   // Draw a color gradient.
     *   for (let x = 0; x < img.width; x += 1) {
     *     for (let y = 0; y < img.height; y += 1) {
     *       let c = map(x, 0, img.width, 0, 255);
     *       img.set(x, y, c);
     *     }
     *   }
     *
     *   // Update the image.
     *   img.updatePixels();
     *
     *   // Display the image.
     *   image(img, 17, 17);
     *
     *   describe('A square with a horiztonal color gradient from black to white drawn on a gray background.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/rockies.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Create a p5.Image object.
     *   let img2 = createImage(100, 100);
     *
     *   // Set the blank image's pixels using the landscape.
     *   img2.set(0, 0, img);
     *
     *   // Display the second image.
     *   image(img2, 0, 0);
     *
     *   describe('An image of a mountain landscape.');
     * }
     */
    set(x, y, imgOrCol) {
      // Renderer2D.prototype.set.call(this, x, y, imgOrCol);
      // round down to get integer numbers
      x = Math.floor(x);
      y = Math.floor(y);
      const pixelsState = this._pixelsState;
      if (imgOrCol instanceof Image) {
        this.drawingContext.save();
        this.drawingContext.setTransform(1, 0, 0, 1, 0, 0);
        this.drawingContext.scale(
          this._pixelDensity,
          this._pixelDensity
        );
        this.drawingContext.clearRect(x, y, imgOrCol.width, imgOrCol.height);
        this.drawingContext.drawImage(imgOrCol.canvas, x, y);
        this.drawingContext.restore();
      } else {
        let r = 0,
          g = 0,
          b = 0,
          a = 0;
        let idx =
          4 *
          (y *
            this._pixelDensity *
            (this.width * this._pixelDensity) +
            x * this._pixelDensity);
        if (!pixelsState.imageData) {
          pixelsState.loadPixels();
        }
        if (typeof imgOrCol === 'number') {
          if (idx < pixelsState.pixels.length) {
            r = imgOrCol;
            g = imgOrCol;
            b = imgOrCol;
            a = 255;
            //this.updatePixels.call(this);
          }
        } else if (Array.isArray(imgOrCol)) {
          if (imgOrCol.length < 4) {
            throw new Error('pixel array must be of the form [R, G, B, A]');
          }
          if (idx < pixelsState.pixels.length) {
            r = imgOrCol[0];
            g = imgOrCol[1];
            b = imgOrCol[2];
            a = imgOrCol[3];
            //this.updatePixels.call(this);
          }
        } else if (imgOrCol instanceof p5.Color) {
          if (idx < pixelsState.pixels.length) {
            [r, g, b, a] = imgOrCol._getRGBA([255, 255, 255, 255]);
            //this.updatePixels.call(this);
          }
        }
        // loop over pixelDensity * pixelDensity
        for (let i = 0; i < this._pixelDensity; i++) {
          for (let j = 0; j < this._pixelDensity; j++) {
            // loop over
            idx =
              4 *
              ((y * this._pixelDensity + j) *
                this.width *
                this._pixelDensity +
                (x * this._pixelDensity + i));
            pixelsState.pixels[idx] = r;
            pixelsState.pixels[idx + 1] = g;
            pixelsState.pixels[idx + 2] = b;
            pixelsState.pixels[idx + 3] = a;
          }
        }
      }
      this.setModified(true);
    }

    /**
     * Resizes the image to a given width and height.
     *
     * The image's original aspect ratio can be kept by passing 0 for either
     * `width` or `height`. For example, calling `img.resize(50, 0)` on an image
     * that was 500 &times; 300 pixels will resize it to 50 &times; 30 pixels.
     *
     * @param {Number} width resized image width.
     * @param {Number} height resized image height.
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/rockies.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Resize the image.
     *   img.resize(50, 100);
     *
     *   // Display the resized image.
     *   image(img, 0, 0);
     *
     *   describe('Two images of a mountain landscape. One copy of the image is squeezed horizontally.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/rockies.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Resize the image, keeping the aspect ratio.
     *   img.resize(0, 30);
     *
     *   // Display the resized image.
     *   image(img, 0, 0);
     *
     *   describe('Two images of a mountain landscape. The small copy of the image covers the top-left corner of the larger image.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/rockies.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Resize the image, keeping the aspect ratio.
     *   img.resize(60, 0);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   describe('Two images of a mountain landscape. The small copy of the image covers the top-left corner of the larger image.');
     * }
     */
    resize(width, height) {
      // Copy contents to a temporary canvas, resize the original
      // and then copy back.
      //
      // There is a faster approach that involves just one copy and swapping the
      // this.canvas reference. We could switch to that approach if (as i think
      // is the case) there an expectation that the user would not hold a
      // reference to the backing canvas of a p5.Image. But since we do not
      // enforce that at the moment, I am leaving in the slower, but safer
      // implementation.

      // auto-resize
      if (width === 0 && height === 0) {
        width = this.canvas.width;
        height = this.canvas.height;
      } else if (width === 0) {
        width = this.canvas.width * height / this.canvas.height;
      } else if (height === 0) {
        height = this.canvas.height * width / this.canvas.width;
      }

      width = Math.floor(width);
      height = Math.floor(height);

      const tempCanvas = document.createElement('canvas');
      tempCanvas.width = width;
      tempCanvas.height = height;

      if (this.gifProperties) {
        const props = this.gifProperties;
        //adapted from github.com/LinusU/resize-image-data
        const nearestNeighbor = (src, dst) => {
          let pos = 0;
          for (let y = 0; y < dst.height; y++) {
            for (let x = 0; x < dst.width; x++) {
              const srcX = Math.floor(x * src.width / dst.width);
              const srcY = Math.floor(y * src.height / dst.height);
              let srcPos = (srcY * src.width + srcX) * 4;
              dst.data[pos++] = src.data[srcPos++]; // R
              dst.data[pos++] = src.data[srcPos++]; // G
              dst.data[pos++] = src.data[srcPos++]; // B
              dst.data[pos++] = src.data[srcPos++]; // A
            }
          }
        };
        for (let i = 0; i < props.numFrames; i++) {
          const resizedImageData = this.drawingContext.createImageData(
            width,
            height
          );
          nearestNeighbor(props.frames[i].image, resizedImageData);
          props.frames[i].image = resizedImageData;
        }
      }

      tempCanvas.getContext('2d').drawImage(
        this.canvas,
        0, 0, this.canvas.width, this.canvas.height,
        0, 0, tempCanvas.width, tempCanvas.height
      );

      // Resize the original canvas, which will clear its contents
      this.canvas.width = this.width = width;
      this.canvas.height = this.height = height;

      //Copy the image back
      this.drawingContext.drawImage(
        tempCanvas,
        0, 0, width, height,
        0, 0, width, height
      );

      if (this.pixels.length > 0) {
        this.loadPixels();
      }

      this.setModified(true);
    }

    /**
     * Copies pixels from a source image to this image.
     *
     * The first parameter, `srcImage`, is an optional
     * <a href="#/p5.Image">p5.Image</a> object to copy. If a source image isn't
     * passed, then `img.copy()` can copy a region of this image to another
     * region.
     *
     * The next four parameters, `sx`, `sy`, `sw`, and `sh` determine the region
     * to copy from the source image. `(sx, sy)` is the top-left corner of the
     * region. `sw` and `sh` are the region's width and height.
     *
     * The next four parameters, `dx`, `dy`, `dw`, and `dh` determine the region
     * of this image to copy into. `(dx, dy)` is the top-left corner of the
     * region. `dw` and `dh` are the region's width and height.
     *
     * Calling `img.copy()` will scale pixels from the source region if it isn't
     * the same size as the destination region.
     *
     * @param  {p5.Image|p5.Element} srcImage source image.
     * @param  {Integer} sx x-coordinate of the source's upper-left corner.
     * @param  {Integer} sy y-coordinate of the source's upper-left corner.
     * @param  {Integer} sw source image width.
     * @param  {Integer} sh source image height.
     * @param  {Integer} dx x-coordinate of the destination's upper-left corner.
     * @param  {Integer} dy y-coordinate of the destination's upper-left corner.
     * @param  {Integer} dw destination image width.
     * @param  {Integer} dh destination image height.
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/rockies.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Copy one region of the image to another.
     *   img.copy(7, 22, 10, 10, 35, 25, 50, 50);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Outline the copied region.
     *   stroke(255);
     *   noFill();
     *   square(7, 22, 10);
     *
     *   describe('An image of a mountain landscape. A square region is outlined in white. A larger square contains a pixelated view of the outlined region.');
     * }
     *
     * @example
     * let mountains;
     * let bricks;
     *
     * async function setup() {
     *   // Load the images.
     *   mountains = await loadImage('assets/rockies.jpg');
     *   bricks = await loadImage('assets/bricks.jpg');
     *   createCanvas(100, 100);
     *
     *   // Calculate the center of the bricks image.
     *   let x = bricks.width / 2;
     *   let y = bricks.height / 2;
     *
     *   // Copy the bricks to the mountains image.
     *   mountains.copy(bricks, 0, 0, x, y, 0, 0, x, y);
     *
     *   // Display the mountains image.
     *   image(mountains, 0, 0);
     *
     *   describe('An image of a brick wall drawn at the top-left of an image of a mountain landscape.');
     * }
     */
    /**
     * @param  {Integer} sx
     * @param  {Integer} sy
     * @param  {Integer} sw
     * @param  {Integer} sh
     * @param  {Integer} dx
     * @param  {Integer} dy
     * @param  {Integer} dw
     * @param  {Integer} dh
     */
    copy(...args) {
      // NOTE: Duplicate implementation here and pixels.js
      let srcImage, sx, sy, sw, sh, dx, dy, dw, dh;
      if (args.length === 9) {
        srcImage = args[0];
        sx = args[1];
        sy = args[2];
        sw = args[3];
        sh = args[4];
        dx = args[5];
        dy = args[6];
        dw = args[7];
        dh = args[8];
      } else if (args.length === 8) {
        srcImage = this;
        sx = args[0];
        sy = args[1];
        sw = args[2];
        sh = args[3];
        dx = args[4];
        dy = args[5];
        dw = args[6];
        dh = args[7];
      } else {
        throw new Error('Signature not supported');
      }

      this._copyHelper(this, srcImage, sx, sy, sw, sh, dx, dy, dw, dh);
    }

    _copyHelper(
      dstImage,
      srcImage,
      sx,
      sy,
      sw,
      sh,
      dx,
      dy,
      dw,
      dh
    ){
      const s = srcImage.canvas.width / srcImage.width;
      // adjust coord system for 3D when renderer
      // ie top-left = -width/2, -height/2
      let sxMod = 0;
      let syMod = 0;
      if (srcImage._renderer && srcImage._renderer.isP3D) {
        sxMod = srcImage.width / 2;
        syMod = srcImage.height / 2;
      }
      if (dstImage._renderer && dstImage._renderer.isP3D) {
        dstImage.push();
        dstImage.resetMatrix();
        dstImage.noLights();
        dstImage.blendMode(dstImage.BLEND);
        dstImage.imageMode(dstImage.CORNER);
        dstImage._renderer.image(
          srcImage,
          sx + sxMod,
          sy + syMod,
          sw,
          sh,
          dx,
          dy,
          dw,
          dh
        );
        dstImage.pop();
      } else {
        dstImage.drawingContext.drawImage(
          srcImage.canvas,
          s * (sx + sxMod),
          s * (sy + syMod),
          s * sw,
          s * sh,
          dx,
          dy,
          dw,
          dh
        );
      }
    }

    /**
     * Masks part of the image with another.
     *
     * `img.mask()` uses another <a href="#/p5.Image">p5.Image</a> object's
     * alpha channel as the alpha channel for this image. Masks are cumulative
     * and can't be removed once applied. If the mask has a different
     * pixel density from this image, the mask will be scaled.
     *
     * @param {p5.Image} srcImage source image.
     *
     * @example
     * let photo;
     * let maskImage;
     *
     * async function setup() {
     *   // Load the images.
     *   photo = await loadImage('assets/rockies.jpg');
     *   maskImage = await loadImage('assets/mask2.png');
     *   createCanvas(100, 100);
     *
     *   // Apply the mask.
     *   photo.mask(maskImage);
     *
     *   // Display the image.
     *   image(photo, 0, 0);
     *
     *   describe('An image of a mountain landscape. The right side of the image has a faded patch of white.');
     * }
     */
    // TODO: - Accept an array of alpha values.
    mask(p5Image) {
      if (p5Image === undefined) {
        p5Image = this;
      }
      const currBlend = this.drawingContext.globalCompositeOperation;

      let imgScaleFactor = this._pixelDensity;
      let maskScaleFactor = 1;
      if (p5Image instanceof Renderer) {
        maskScaleFactor = p5Image._pInst._renderer._pixelDensity;
      }

      const copyArgs = [
        p5Image,
        0,
        0,
        maskScaleFactor * p5Image.width,
        maskScaleFactor * p5Image.height,
        0,
        0,
        imgScaleFactor * this.width,
        imgScaleFactor * this.height
      ];

      this.drawingContext.globalCompositeOperation = 'destination-in';
      if (this.gifProperties) {
        for (let i = 0; i < this.gifProperties.frames.length; i++) {
          this.drawingContext.putImageData(
            this.gifProperties.frames[i].image,
            0,
            0
          );
          this.copy(...copyArgs);
          this.gifProperties.frames[i].image = this.drawingContext.getImageData(
            0,
            0,
            imgScaleFactor * this.width,
            imgScaleFactor * this.height
          );
        }
        this.drawingContext.putImageData(
          this.gifProperties.frames[this.gifProperties.displayIndex].image,
          0,
          0
        );
      } else {
        this.copy(...copyArgs);
      }
      this.drawingContext.globalCompositeOperation = currBlend;
      this.setModified(true);
    }

    /**
     * Applies an image filter to the image.
     *
     * The preset options are:
     *
     * `INVERT`
     * Inverts the colors in the image. No parameter is used.
     *
     * `GRAY`
     * Converts the image to grayscale. No parameter is used.
     *
     * `THRESHOLD`
     * Converts the image to black and white. Pixels with a grayscale value
     * above a given threshold are converted to white. The rest are converted to
     * black. The threshold must be between 0.0 (black) and 1.0 (white). If no
     * value is specified, 0.5 is used.
     *
     * `OPAQUE`
     * Sets the alpha channel to be entirely opaque. No parameter is used.
     *
     * `POSTERIZE`
     * Limits the number of colors in the image. Each color channel is limited to
     * the number of colors specified. Values between 2 and 255 are valid, but
     * results are most noticeable with lower values. The default value is 4.
     *
     * `BLUR`
     * Blurs the image. The level of blurring is specified by a blur radius. Larger
     * values increase the blur. The default value is 4. A gaussian blur is used
     * in `P2D` mode. A box blur is used in `WEBGL` mode.
     *
     * `ERODE`
     * Reduces the light areas. No parameter is used.
     *
     * `DILATE`
     * Increases the light areas. No parameter is used.
     *
     * @param  {(THRESHOLD|GRAY|OPAQUE|INVERT|POSTERIZE|ERODE|DILATE|BLUR)} filterType  either THRESHOLD, GRAY, OPAQUE, INVERT,
     *                                POSTERIZE, ERODE, DILATE or BLUR.
     * @param  {Number} [filterParam] parameter unique to each filter.
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Apply the INVERT filter.
     *   img.filter(INVERT);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   describe('A blue brick wall.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Apply the GRAY filter.
     *   img.filter(GRAY);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   describe('A brick wall drawn in grayscale.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Apply the THRESHOLD filter.
     *   img.filter(THRESHOLD);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   describe('A brick wall drawn in black and white.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Apply the OPAQUE filter.
     *   img.filter(OPAQUE);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   describe('A red brick wall.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Apply the POSTERIZE filter.
     *   img.filter(POSTERIZE, 3);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   describe('An image of a red brick wall drawn with a limited color palette.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Apply the BLUR filter.
     *   img.filter(BLUR, 3);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   describe('A blurry image of a red brick wall.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Apply the DILATE filter.
     *   img.filter(DILATE);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   describe('A red brick wall with bright lines between each brick.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Apply the ERODE filter.
     *   img.filter(ERODE);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   describe('A red brick wall with faint lines between each brick.');
     * }
     */
    filter(operation, value) {
      Filters.apply(this.canvas, Filters[operation], value);
      this.setModified(true);
    }

    /**
     * Copies a region of pixels from another image into this one.
     *
     * The first parameter, `srcImage`, is the
     * <a href="#/p5.Image">p5.Image</a> object to blend.
     *
     * The next four parameters, `sx`, `sy`, `sw`, and `sh` determine the region
     * to blend from the source image. `(sx, sy)` is the top-left corner of the
     * region. `sw` and `sh` are the regions width and height.
     *
     * The next four parameters, `dx`, `dy`, `dw`, and `dh` determine the region
     * of the canvas to blend into. `(dx, dy)` is the top-left corner of the
     * region. `dw` and `dh` are the regions width and height.
     *
     * The tenth parameter, `blendMode`, sets the effect used to blend the images'
     * colors. The options are `BLEND`, `DARKEST`, `LIGHTEST`, `DIFFERENCE`,
     * `MULTIPLY`, `EXCLUSION`, `SCREEN`, `REPLACE`, `OVERLAY`, `HARD_LIGHT`,
     * `SOFT_LIGHT`, `DODGE`, `BURN`, `ADD`, or `NORMAL`.
     *
     * @param  {p5.Image} srcImage source image
     * @param  {Integer} sx x-coordinate of the source's upper-left corner.
     * @param  {Integer} sy y-coordinate of the source's upper-left corner.
     * @param  {Integer} sw source image width.
     * @param  {Integer} sh source image height.
     * @param  {Integer} dx x-coordinate of the destination's upper-left corner.
     * @param  {Integer} dy y-coordinate of the destination's upper-left corner.
     * @param  {Integer} dw destination image width.
     * @param  {Integer} dh destination image height.
     * @param  {(BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL)} blendMode the blend mode. either
     *     BLEND, DARKEST, LIGHTEST, DIFFERENCE,
     *     MULTIPLY, EXCLUSION, SCREEN, REPLACE, OVERLAY, HARD_LIGHT,
     *     SOFT_LIGHT, DODGE, BURN, ADD or NORMAL.
     *
     * Available blend modes are: normal | multiply | screen | overlay |
     *            darken | lighten | color-dodge | color-burn | hard-light |
     *            soft-light | difference | exclusion | hue | saturation |
     *            color | luminosity
     *
     * http://blogs.adobe.com/webplatform/2013/01/28/blending-features-in-canvas/
     *
     * @example
     * let mountains;
     * let bricks;
     *
     * async function setup() {
     *   // Load the images.
     *   mountains = await loadImage('assets/rockies.jpg');
     *   bricks = await loadImage('assets/bricks_third.jpg');
     *   createCanvas(100, 100);
     *
     *   // Blend the bricks image into the mountains.
     *   mountains.blend(bricks, 0, 0, 33, 100, 67, 0, 33, 100, ADD);
     *
     *   // Display the mountains image.
     *   image(mountains, 0, 0);
     *
     *   // Display the bricks image.
     *   image(bricks, 0, 0);
     *
     *   describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears faded on the right of the image.');
     * }
     *
     * @example
     * let mountains;
     * let bricks;
     *
     * async function setup() {
     *   // Load the images.
     *   mountains = await loadImage('assets/rockies.jpg');
     *   bricks = await loadImage('assets/bricks_third.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Blend the bricks image into the mountains.
     *   mountains.blend(bricks, 0, 0, 33, 100, 67, 0, 33, 100, DARKEST);
     *
     *   // Display the mountains image.
     *   image(mountains, 0, 0);
     *
     *   // Display the bricks image.
     *   image(bricks, 0, 0);
     *
     *   describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears transparent on the right of the image.');
     * }
     *
     * @example
     * let mountains;
     * let bricks;
     *
     * async function setup() {
     *   // Load the images.
     *   mountains = await loadImage('assets/rockies.jpg');
     *   bricks = await loadImage('assets/bricks_third.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Blend the bricks image into the mountains.
     *   mountains.blend(bricks, 0, 0, 33, 100, 67, 0, 33, 100, LIGHTEST);
     *
     *   // Display the mountains image.
     *   image(mountains, 0, 0);
     *
     *   // Display the bricks image.
     *   image(bricks, 0, 0);
     *
     *   describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears washed out on the right of the image.');
     * }
     */
    /**
     * @param  {Integer} sx
     * @param  {Integer} sy
     * @param  {Integer} sw
     * @param  {Integer} sh
     * @param  {Integer} dx
     * @param  {Integer} dy
     * @param  {Integer} dw
     * @param  {Integer} dh
     * @param  {(BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL)} blendMode
     */
    blend(...args) {
      const currBlend = this.drawingContext.globalCompositeOperation;
      const blendMode = args[args.length - 1];
      const copyArgs = Array.prototype.slice.call(args, 0, args.length - 1);

      this.drawingContext.globalCompositeOperation = blendMode;
      this.copy(...copyArgs);
      this.drawingContext.globalCompositeOperation = currBlend;
      this.setModified(true);
    }

    /**
     * helper method for web GL mode to indicate that an image has been
     * changed or unchanged since last upload. gl texture upload will
     * set this value to false after uploading the texture.
     * @param {Boolean} val sets whether or not the image has been
     * modified.
     * @private
     */
    setModified(val) {
      this._modified = val; //enforce boolean?
    }

    /**
     * helper method for web GL mode to figure out if the image
     * has been modified and might need to be re-uploaded to texture
     * memory between frames.
     * @private
     * @return {boolean} a boolean indicating whether or not the
     * image has been updated or modified since last texture upload.
     */
    isModified() {
      return this._modified;
    }

    /**
     * Saves the image to a file.
     *
     * By default, `img.save()` saves the image as a PNG image called
     * `untitled.png`.
     *
     * The first parameter, `filename`, is optional. It's a string that sets the
     * file's name. If a file extension is included, as in
     * `img.save('drawing.png')`, then the image will be saved using that
     * format.
     *
     * The second parameter, `extension`, is also optional. It sets the files format.
     * Either `'png'` or `'jpg'` can be used. For example, `img.save('drawing', 'jpg')`
     * saves the canvas to a file called `drawing.jpg`.
     *
     * Note: The browser will either save the file immediately or prompt the user
     * with a dialogue window.
     *
     * The image will only be downloaded as an animated GIF if it was loaded
     * from a GIF file. See <a href="#/p5/saveGif">saveGif()</a> to create new
     * GIFs.
     *
     * @param {String} filename filename. Defaults to 'untitled'.
     * @param  {String} [extension] file extension, either 'png' or 'jpg'.
     *                            Defaults to 'png'.
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/rockies.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   describe('An image of a mountain landscape. The image is downloaded when the user presses the "s", "j", or "p" key.');
     * }
     *
     * // Save the image with different options when the user presses a key.
     * function keyPressed() {
     *   if (key === 's') {
     *     img.save();
     *   } else if (key === 'j') {
     *     img.save('rockies.jpg');
     *   } else if (key === 'p') {
     *     img.save('rockies', 'png');
     *   }
     * }
     */
    save(filename, extension) {
      if (this.gifProperties) {
        encodeAndDownloadGif(this, filename);
      } else {
        let htmlCanvas = this.canvas;
        extension =
          extension ||
          _checkFileExtension(filename, extension)[1] ||
          'png';

        let mimeType;
        switch (extension) {
          default:
            //case 'png':
            mimeType = 'image/png';
            break;
          case 'webp':
            mimeType = 'image/webp';
            break;
          case 'jpeg':
          case 'jpg':
            mimeType = 'image/jpeg';
            break;
        }

        htmlCanvas.toBlob(blob => {
          downloadFile(blob, filename, extension);
        }, mimeType);
      }
    }

    async toBlob() {
      return new Promise(resolve => {
        this.canvas.toBlob(resolve);
      });
    }

    // GIF Section
    /**
     * Restarts an animated GIF at its first frame.
     *
     * @example
     * let gif;
     *
     * async function setup() {
     *   // Load the image.
     *   gif = await loadImage('assets/arnott-wallace-wink-loop-once.gif');
     *
     *   createCanvas(100, 100);
     *
     *   describe('A cartoon face winks once and then freezes. Clicking resets the face and makes it wink again.');
     * }
     *
     * function draw() {
     *   background(255);
     *
     *   // Display the image.
     *   image(gif, 0, 0);
     * }
     *
     * // Reset the GIF when the user presses the mouse.
     * function mousePressed() {
     *   gif.reset();
     * }
     */
    reset() {
      if (this.gifProperties) {
        const props = this.gifProperties;
        props.playing = true;
        props.timeSinceStart = 0;
        props.timeDisplayed = 0;
        props.lastChangeTime = 0;
        props.loopCount = 0;
        props.displayIndex = 0;
        this.drawingContext.putImageData(props.frames[0].image, 0, 0);
      }
    }

    /**
     * Gets the index of the current frame in an animated GIF.
     *
     * @return {Number}       index of the GIF's current frame.
     *
     * @example
     * let gif;
     *
     * async function setup() {
     *   // Load the image.
     *   gif = await loadImage('assets/arnott-wallace-eye-loop-forever.gif');
     *
     *   createCanvas(100, 100);
     *
     *   describe('A cartoon eye repeatedly looks around, then outwards. A number displayed in the bottom-left corner increases from 0 to 124, then repeats.');
     * }
     *
     * function draw() {
     *   // Get the index of the current GIF frame.
     *   let index = gif.getCurrentFrame();
     *
     *   // Display the image.
     *   image(gif, 0, 0);
     *
     *   // Display the current frame.
     *   text(index, 10, 90);
     * }
     */
    getCurrentFrame() {
      if (this.gifProperties) {
        const props = this.gifProperties;
        return props.displayIndex % props.numFrames;
      }
    }

    /**
     * Sets the current frame in an animated GIF.
     *
     * @param {Number} index index of the frame to display.
     *
     * @example
     * let gif;
     * let frameSlider;
     *
     * async function setup() {
     *   // Load the image.
     *   gif = await loadImage('assets/arnott-wallace-eye-loop-forever.gif');
     *
     *   createCanvas(100, 100);
     *
     *   // Get the index of the last frame.
     *   let maxFrame = gif.numFrames() - 1;
     *
     *   // Create a slider to control which frame is drawn.
     *   frameSlider = createSlider(0, maxFrame);
     *   frameSlider.position(10, 80);
     *   frameSlider.size(80);
     *
     *   describe('A cartoon eye looks around when a slider is moved.');
     * }
     *
     * function draw() {
     *   // Get the slider's value.
     *   let index = frameSlider.value();
     *
     *   // Set the GIF's frame.
     *   gif.setFrame(index);
     *
     *   // Display the image.
     *   image(gif, 0, 0);
     * }
     */
    setFrame(index) {
      if (this.gifProperties) {
        const props = this.gifProperties;
        if (index < props.numFrames && index >= 0) {
          props.timeDisplayed = 0;
          props.lastChangeTime = 0;
          props.displayIndex = index;
          this.drawingContext.putImageData(props.frames[index].image, 0, 0);
        } else {
          console.log(
            'Cannot set GIF to a frame number that is higher than total number of frames or below zero.'
          );
        }
      }
    }

    /**
     * Returns the number of frames in an animated GIF.
     *
     * @return {Number} number of frames in the GIF.
     *
     * @example
     * let gif;
     *
     * async function setup() {
     *   // Load the image.
     *   gif = await loadImage('assets/arnott-wallace-eye-loop-forever.gif');
     *
     *   createCanvas(100, 100);
     *
     *   describe('A cartoon eye looks around. The text "n / 125" is shown at the bottom of the canvas.');
     * }
     *
     * function draw() {
     *   // Display the image.
     *   image(gif, 0, 0);
     *
     *   // Display the current state of playback.
     *   let total = gif.numFrames();
     *   let index = gif.getCurrentFrame();
     *   text(`${index} / ${total}`, 30, 90);
     * }
     */
    numFrames() {
      if (this.gifProperties) {
        return this.gifProperties.numFrames;
      }
    }

    /**
     * Plays an animated GIF that was paused with
     * <a href="#/p5.Image/pause">img.pause()</a>.
     *
     * @example
     * let gif;
     *
     * async function setup() {
     *   // Load the image.
     *   gif = await loadImage('assets/nancy-liang-wind-loop-forever.gif');
     *
     *   createCanvas(100, 100);
     *
     *   describe('A drawing of a child with hair blowing in the wind. The animation freezes when clicked and resumes when released.');
     * }
     *
     * function draw() {
     *   background(255);
     *   image(gif, 0, 0);
     * }
     *
     * // Pause the GIF when the user presses the mouse.
     * function mousePressed() {
     *   gif.pause();
     * }
     *
     * // Play the GIF when the user releases the mouse.
     * function mouseReleased() {
     *   gif.play();
     * }
     */
    play() {
      if (this.gifProperties) {
        this.gifProperties.playing = true;
      }
    }

    /**
     * Pauses an animated GIF.
     *
     * The GIF can be resumed by calling
     * <a href="#/p5.Image/play">img.play()</a>.
     *
     * @example
     * let gif;
     *
     * async function setup() {
     *   // Load the image.
     *   gif = await loadImage('assets/nancy-liang-wind-loop-forever.gif');
     *
     *   createCanvas(100, 100);
     *
     *   describe('A drawing of a child with hair blowing in the wind. The animation freezes when clicked and resumes when released.');
     * }
     *
     * function draw() {
     *   background(255);
     *
     *   // Display the image.
     *   image(gif, 0, 0);
     * }
     *
     * // Pause the GIF when the user presses the mouse.
     * function mousePressed() {
     *   gif.pause();
     * }
     *
     * // Play the GIF when the user presses the mouse.
     * function mouseReleased() {
     *   gif.play();
     * }
     */
    pause() {
      if (this.gifProperties) {
        this.gifProperties.playing = false;
      }
    }

    /**
     * Changes the delay between frames in an animated GIF.
     *
     * The first parameter, `delay`, is the length of the delay in milliseconds.
     *
     * The second parameter, `index`, is optional. If provided, only the frame
     * at `index` will have its delay modified. All other frames will keep
     * their default delay.
     *
     * @param {Number} d delay in milliseconds between switching frames.
     * @param {Number} [index] index of the frame that will have its delay modified.
     *
     * @example
     * let gifFast;
     * let gifSlow;
     *
     * async function setup() {
     *   // Load the images.
     *   gifFast = await loadImage('assets/arnott-wallace-eye-loop-forever.gif');
     *   gifSlow = await loadImage('assets/arnott-wallace-eye-loop-forever.gif');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Resize the images.
     *   gifFast.resize(50, 50);
     *   gifSlow.resize(50, 50);
     *
     *   // Set the delay lengths.
     *   gifFast.delay(10);
     *   gifSlow.delay(100);
     *
     *   describe('Two animated eyes looking around. The eye on the left moves faster than the eye on the right.');
     * }
     *
     * function draw() {
     *   // Display the images.
     *   image(gifFast, 0, 0);
     *   image(gifSlow, 50, 0);
     * }
     *
     * @example
     * let gif;
     *
     * async function setup() {
     *   // Load the image.
     *   gif = await loadImage('assets/arnott-wallace-eye-loop-forever.gif');
     *
     *   createCanvas(100, 100);
     *
     *   // Set the delay of frame 67.
     *   gif.delay(3000, 67);
     *
     *   describe('An animated eye looking around. It pauses for three seconds while it looks down.');
     * }
     *
     * function draw() {
     *   // Display the image.
     *   image(gif, 0, 0);
     * }
     */
    delay(d, index) {
      if (this.gifProperties) {
        const props = this.gifProperties;
        if (index < props.numFrames && index >= 0) {
          props.frames[index].delay = d;
        } else {
          // change all frames
          for (const frame of props.frames) {
            frame.delay = d;
          }
        }
      }
    }
  }
  function encodeAndDownloadGif(pImg, filename) {
    const props = pImg.gifProperties;

    //convert loopLimit back into Netscape Block formatting
    let loopLimit = props.loopLimit;
    if (loopLimit === 1) {
      loopLimit = null;
    } else if (loopLimit === null) {
      loopLimit = 0;
    }
    const buffer = new Uint8Array(pImg.width * pImg.height * props.numFrames);

    const allFramesPixelColors = [];

    // Used to determine the occurrence of unique palettes and the frames
    // which use them
    const paletteFreqsAndFrames = {};

    // Pass 1:
    //loop over frames and get the frequency of each palette
    for (let i = 0; i < props.numFrames; i++) {
      const paletteSet = new Set();
      const data = props.frames[i].image.data;
      const dataLength = data.length;
      // The color for each pixel in this frame ( for easier lookup later )
      const pixelColors = new Uint32Array(pImg.width * pImg.height);
      for (let j = 0, k = 0; j < dataLength; j += 4, k++) {
        const r = data[j + 0];
        const g = data[j + 1];
        const b = data[j + 2];
        const color = (r << 16) | (g << 8) | (b << 0);
        paletteSet.add(color);

        // What color does this pixel have in this frame ?
        pixelColors[k] = color;
      }

      // A way to put use the entire palette as an object key
      const paletteStr = [...paletteSet].sort().toString();
      if (paletteFreqsAndFrames[paletteStr] === undefined) {
        paletteFreqsAndFrames[paletteStr] = { freq: 1, frames: [i] };
      } else {
        paletteFreqsAndFrames[paletteStr].freq += 1;
        paletteFreqsAndFrames[paletteStr].frames.push(i);
      }

      allFramesPixelColors.push(pixelColors);
    }

    let framesUsingGlobalPalette = [];

    // Now to build the global palette
    // Sort all the unique palettes in descending order of their occurrence
    const palettesSortedByFreq = Object.keys(paletteFreqsAndFrames).sort(function(
      a,
      b
    ) {
      return paletteFreqsAndFrames[b].freq - paletteFreqsAndFrames[a].freq;
    });

    // The initial global palette is the one with the most occurrence
    const globalPalette = palettesSortedByFreq[0]
      .split(',')
      .map(a => parseInt(a));

    framesUsingGlobalPalette = framesUsingGlobalPalette.concat(
      paletteFreqsAndFrames[globalPalette].frames
    );

    const globalPaletteSet = new Set(globalPalette);

    // Build a more complete global palette
    // Iterate over the remaining palettes in the order of
    // their occurrence and see if the colors in this palette which are
    // not in the global palette can be added there, while keeping the length
    // of the global palette <= 256
    for (let i = 1; i < palettesSortedByFreq.length; i++) {
      const palette = palettesSortedByFreq[i].split(',').map(a => parseInt(a));

      const difference = palette.filter(x => !globalPaletteSet.has(x));
      if (globalPalette.length + difference.length <= 256) {
        for (let j = 0; j < difference.length; j++) {
          globalPalette.push(difference[j]);
          globalPaletteSet.add(difference[j]);
        }

        // All frames using this palette now use the global palette
        framesUsingGlobalPalette = framesUsingGlobalPalette.concat(
          paletteFreqsAndFrames[palettesSortedByFreq[i]].frames
        );
      }
    }

    framesUsingGlobalPalette = new Set(framesUsingGlobalPalette);

    // Build a lookup table of the index of each color in the global palette
    // Maps a color to its index
    const globalIndicesLookup = {};
    for (let i = 0; i < globalPalette.length; i++) {
      if (!globalIndicesLookup[globalPalette[i]]) {
        globalIndicesLookup[globalPalette[i]] = i;
      }
    }

    // force palette to be power of 2
    let powof2 = 1;
    while (powof2 < globalPalette.length) {
      powof2 <<= 1;
    }
    globalPalette.length = powof2;

    // global opts
    const opts = {
      loop: loopLimit,
      palette: new Uint32Array(globalPalette)
    };
    const gifWriter = new omggif.GifWriter(buffer, pImg.width, pImg.height, opts);
    let previousFrame = {};

    // Pass 2
    // Determine if the frame needs a local palette
    // Also apply transparency optimization. This function will often blow up
    // the size of a GIF if not for transparency. If a pixel in one frame has
    // the same color in the previous frame, that pixel can be marked as
    // transparent. We decide one particular color as transparent and make all
    // transparent pixels take this color. This helps in later in compression.
    for (let i = 0; i < props.numFrames; i++) {
      const localPaletteRequired = !framesUsingGlobalPalette.has(i);
      const palette = localPaletteRequired ? [] : globalPalette;
      const pixelPaletteIndex = new Uint8Array(pImg.width * pImg.height);

      // Lookup table mapping color to its indices
      const colorIndicesLookup = {};

      // All the colors that cannot be marked transparent in this frame
      const cannotBeTransparent = new Set();

      allFramesPixelColors[i].forEach((color, k) => {
        if (localPaletteRequired) {
          if (colorIndicesLookup[color] === undefined) {
            colorIndicesLookup[color] = palette.length;
            palette.push(color);
          }
          pixelPaletteIndex[k] = colorIndicesLookup[color];
        } else {
          pixelPaletteIndex[k] = globalIndicesLookup[color];
        }

        if (i > 0) {
          // If even one pixel of this color has changed in this frame
          // from the previous frame, we cannot mark it as transparent
          if (allFramesPixelColors[i - 1][k] !== color) {
            cannotBeTransparent.add(color);
          }
        }
      });

      const frameOpts = {};

      // Transparency optimization
      const canBeTransparent = palette.filter(a => !cannotBeTransparent.has(a));
      if (canBeTransparent.length > 0) {
        // Select a color to mark as transparent
        const transparent = canBeTransparent[0];
        const transparentIndex = localPaletteRequired
          ? colorIndicesLookup[transparent]
          : globalIndicesLookup[transparent];
        if (i > 0) {
          for (let k = 0; k < allFramesPixelColors[i].length; k++) {
            // If this pixel in this frame has the same color in previous frame
            if (allFramesPixelColors[i - 1][k] === allFramesPixelColors[i][k]) {
              pixelPaletteIndex[k] = transparentIndex;
            }
          }
          frameOpts.transparent = transparentIndex;
          // If this frame has any transparency, do not dispose the previous frame
          previousFrame.frameOpts.disposal = 1;
        }
      }
      frameOpts.delay = props.frames[i].delay / 10; // Move timing back into GIF formatting
      if (localPaletteRequired) {
        // force palette to be power of 2
        let powof2 = 1;
        while (powof2 < palette.length) {
          powof2 <<= 1;
        }
        palette.length = powof2;
        frameOpts.palette = new Uint32Array(palette);
      }
      if (i > 0) {
        // add the frame that came before the current one
        gifWriter.addFrame(
          0,
          0,
          pImg.width,
          pImg.height,
          previousFrame.pixelPaletteIndex,
          previousFrame.frameOpts
        );
      }
      // previous frame object should now have details of this frame
      previousFrame = {
        pixelPaletteIndex,
        frameOpts
      };
    }

    previousFrame.frameOpts.disposal = 1;
    // add the last frame
    gifWriter.addFrame(
      0,
      0,
      pImg.width,
      pImg.height,
      previousFrame.pixelPaletteIndex,
      previousFrame.frameOpts
    );

    const extension = 'gif';
    const blob = new Blob([buffer.slice(0, gifWriter.end())], {
      type: 'image/gif'
    });
    downloadFile(blob, filename, extension);
  }
  function image$2(p5, fn){
    /**
     * A class to describe an image.
     *
     * Images are rectangular grids of pixels that can be displayed and modified.
     *
     * Existing images can be loaded by calling
     * <a href="#/p5/loadImage">loadImage()</a>. Blank images can be created by
     * calling <a href="#/p5/createImage">createImage()</a>. `p5.Image` objects
     * have methods for common tasks such as applying filters and modifying
     * pixel values.
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   describe('An image of a brick wall.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Apply the GRAY filter.
     *   img.filter(GRAY);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   describe('A grayscale image of a brick wall.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Image object.
     *   let img = createImage(66, 66);
     *
     *   // Load the image's pixels.
     *   img.loadPixels();
     *
     *   // Set the pixels to black.
     *   for (let x = 0; x < img.width; x += 1) {
     *     for (let y = 0; y < img.height; y += 1) {
     *       img.set(x, y, 0);
     *     }
     *   }
     *
     *   // Update the image.
     *   img.updatePixels();
     *
     *   // Display the image.
     *   image(img, 17, 17);
     *
     *   describe('A black square drawn in the middle of a gray square.');
     * }
     *
     * @class p5.Image
     * @param {Number} width
     * @param {Number} height
     */
    p5.Image = Image;

    /**
     * The image's width in pixels.
     *
     * @type {Number}
     * @property {Number} width
     * @for p5.Image
     * @name width
     * @readOnly
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/rockies.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Calculate the center coordinates.
     *   let x = img.width / 2;
     *   let y = img.height / 2;
     *
     *   // Draw a circle at the image's center.
     *   circle(x, y, 20);
     *
     *   describe('An image of a mountain landscape with a white circle drawn in the middle.');
     * }
     */

    /**
     * The image's height in pixels.
     *
     * @type {Number}
     * @property height
     * @for p5.Image
     * @name height
     * @readOnly
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/rockies.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Calculate the center coordinates.
     *   let x = img.width / 2;
     *   let y = img.height / 2;
     *
     *   // Draw a circle at the image's center.
     *   circle(x, y, 20);
     *
     *   describe('An image of a mountain landscape with a white circle drawn in the middle.');
     * }
     */

    /**
     * An array containing the color of each pixel in the image.
     *
     * Colors are stored as numbers representing red, green, blue, and alpha
     * (RGBA) values. `img.pixels` is a one-dimensional array for performance
     * reasons.
     *
     * Each pixel occupies four elements in the pixels array, one for each
     * RGBA value. For example, the pixel at coordinates (0, 0) stores its
     * RGBA values at `img.pixels[0]`, `img.pixels[1]`, `img.pixels[2]`,
     * and `img.pixels[3]`, respectively. The next pixel at coordinates (1, 0)
     * stores its RGBA values at `img.pixels[4]`, `img.pixels[5]`,
     * `img.pixels[6]`, and `img.pixels[7]`. And so on. The `img.pixels` array
     * for a 100×100 <a href="#/p5.Image">p5.Image</a> object has
     * 100 × 100 × 4 = 40,000 elements.
     *
     * Accessing the RGBA values for a pixel in the image requires a little
     * math as shown in the examples below. The
     * <a href="#/p5.Image/loadPixels">img.loadPixels()</a>
     * method must be called before accessing the `img.pixels` array. The
     * <a href="#/p5.Image/updatePixels">img.updatePixels()</a> method must be
     * called after any changes are made.
     *
     * @property {Number[]} pixels
     * @for p5.Image
     * @name pixels
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Image object.
     *   let img = createImage(66, 66);
     *
     *   // Load the image's pixels.
     *   img.loadPixels();
     *
     *   for (let i = 0; i < img.pixels.length; i += 4) {
     *     // Red.
     *     img.pixels[i] = 0;
     *     // Green.
     *     img.pixels[i + 1] = 0;
     *     // Blue.
     *     img.pixels[i + 2] = 0;
     *     // Alpha.
     *     img.pixels[i + 3] = 255;
     *   }
     *
     *   // Update the image.
     *   img.updatePixels();
     *
     *   // Display the image.
     *   image(img, 17, 17);
     *
     *   describe('A black square drawn in the middle of a gray square.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Image object.
     *   let img = createImage(66, 66);
     *
     *   // Load the image's pixels.
     *   img.loadPixels();
     *
     *   // Set the pixels to red.
     *   for (let i = 0; i < img.pixels.length; i += 4) {
     *     // Red.
     *     img.pixels[i] = 255;
     *     // Green.
     *     img.pixels[i + 1] = 0;
     *     // Blue.
     *     img.pixels[i + 2] = 0;
     *     // Alpha.
     *     img.pixels[i + 3] = 255;
     *   }
     *
     *   // Update the image.
     *   img.updatePixels();
     *
     *   // Display the image.
     *   image(img, 17, 17);
     *
     *   describe('A red square drawn in the middle of a gray square.');
     * }
     */
  }

  if(typeof p5 !== 'undefined'){
    image$2(p5);
  }

  /**
   * @module Math
   * @requires constants
   */


  /// HELPERS FOR REMAINDER METHOD
  const calculateRemainder2D = function (xComponent, yComponent) {
    if (xComponent !== 0) {
      this.x = this.x % xComponent;
    }
    if (yComponent !== 0) {
      this.y = this.y % yComponent;
    }
    return this;
  };

  const calculateRemainder3D = function (xComponent, yComponent, zComponent) {
    if (xComponent !== 0) {
      this.x = this.x % xComponent;
    }
    if (yComponent !== 0) {
      this.y = this.y % yComponent;
    }
    if (zComponent !== 0) {
      this.z = this.z % zComponent;
    }
    return this;
  };

  class Vector {
    // This is how it comes in with createVector()
    // This check if the first argument is a function
    constructor(...args) {
      let values = args; // .map(arg => arg || 0);
      if (typeof args[0] === 'function') {
        this.isPInst = true;
        this._fromRadians = args[0];
        this._toRadians = args[1];
        values = args.slice(2); // .map(arg => arg || 0);
      }
      let dimensions = values.length; // TODO: make default 3 if no arguments
      if (dimensions === 0) {
        this.dimensions = 2;
        this._values = [0, 0, 0];
      } else {
        this.dimensions = dimensions;
        this._values = values;
      }
    }

    /**
     * Gets the values of the N-dimensional vector.
     *
     * This method returns an array of numbers that represent the vector.
     * Each number in the array corresponds to a different component of the vector,
     * like its position in different directions (e.g., x, y, z).
     *
     * @returns {Array<number>} The array of values representing the vector.
     */
    get values() {
      return this._values;
    }

    /**
     * Sets the values of the vector.
     *
     * This method allows you to update the entire vector with a new set of values.
     * You need to provide an array of numbers, where each number represents a component
     * of the vector (e.g., x, y, z). The length of the array should match the number of
     * dimensions of the vector. If the array is shorter, the missing components will be
     * set to 0. If the array is longer, the extra values will be ignored.
     *
     * @param {Array<number>} newValues - An array of numbers representing the new values for the vector.
     *
     */
    set values(newValues) {
      let dimensions = newValues.length;
      if (dimensions === 0) {
        this.dimensions = 2;
        this._values = [0, 0, 0];
      } else {
        this.dimensions = dimensions;
        this._values = newValues.slice();
      }
    }

    /**
     * Gets the x component of the vector.
     *
     * This method returns the value of the x component of the vector.
     * Think of the x component as the horizontal position or the first number in the vector.
     * If the x component is not defined, it will return 0.
     *
     * @returns {Number} The x component of the vector. Returns 0 if the value is not defined.
     */
    get x() {
      return this._values[0] || 0;
    }

    /**
     * Retrieves the value at the specified index from the vector.
     *
     * This method allows you to get the value of a specific component of the vector
     * by providing its index. Think of the vector as a list of numbers, where each
     * number represents a different direction (like x, y, or z). The index is just
     * the position of the number in that list.
     *
     * For example, if you have a vector with values 10, 20, 30 the index 0 would
     * give you the first value 10, index 1 would give you the second value 20,
     * and so on.
     *
     * @param {Number} index - The position of the value you want to get from the vector.
     * @returns {Number} The value at the specified position in the vector.
     * @throws Will throw an error if the index is out of bounds, meaning if you try to
     *          get a value from a position that doesn't exist in the vector.
     */
    getValue(index) {
      if (index < this._values.length) {
        return this._values[index];
      } else {
        p5._friendlyError(
          'The index parameter is trying to set a value outside the bounds of the vector',
          'p5.Vector.setValue'
        );
      }
    }

    /**
     * Sets the value at the specified index of the vector.
     *
     * This method allows you to change a specific component of the vector by providing its index and the new value you want to set.
     * Think of the vector as a list of numbers, where each number represents a different direction (like x, y, or z).
     * The index is just the position of the number in that list.
     *
     * For example, if you have a vector with values [0, 20, 30], and you want to change the second value (20) to 50,
     * you would use this method with index 1 (since indexes start at 0) and value 50.
     *
     * @param {Number} index - The position in the vector where you want to set the new value.
     * @param {Number} value - The new value you want to set at the specified position.
     * @throws Will throw an error if the index is outside the bounds of the vector, meaning if you try to set a value at a position that doesn't exist in the vector.
     */
    setValue(index, value) {
      if (index < this._values.length) {
        this._values[index] = value;
      } else {
        p5._friendlyError(
          'The index parameter is trying to set a value outside the bounds of the vector',
          'p5.Vector.setValue'
        );
      }
    }

    /**
     * Gets the y component of the vector.
     *
     * This method returns the value of the y component of the vector.
     * Think of the y component as the vertical position or the second number in the vector.
     * If the y component is not defined, it will return 0.
     *
     * @returns {Number} The y component of the vector. Returns 0 if the value is not defined.
     */
    get y() {
      return this._values[1] || 0;
    }

    /**
     * Gets the z component of the vector.
     *
     * This method returns the value of the z component of the vector.
     * Think of the z component as the depth or the third number in the vector.
     * If the z component is not defined, it will return 0.
     *
     * @returns {Number} The z component of the vector. Returns 0 if the value is not defined.
     */
    get z() {
      return this._values[2] || 0;
    }

    /**
     * Gets the w component of the vector.
     *
     * This method returns the value of the w component of the vector.
     * Think of the w component as the fourth number in the vector.
     * If the w component is not defined, it will return 0.
     *
     * @returns {Number} The w component of the vector. Returns 0 if the value is not defined.
     */
    get w() {
      return this._values[3] || 0;
    }

    /**
     * Sets the x component of the vector.
     *
     * This method allows you to change the x value of the vector.
     * The x value is the first number in the vector, representing the horizontal position.
     * By calling this method, you can update the x value to a new number.
     *
     * @param {Number} xVal - The new value for the x component.
     */
    set x(xVal) {
      if (this._values.length > 1) {
        this._values[0] = xVal;
      }
    }

    /**
     * Sets the y component of the vector.
     *
     * This method allows you to change the y value of the vector.
     * The y value is the second number in the vector, representing the vertical position.
     * By calling this method, you can update the y value to a new number.
     *
     * @param {Number} yVal - The new value for the y component.
     */
    set y(yVal) {
      if (this._values.length > 1) {
        this._values[1] = yVal;
      }
    }

    /**
     * Sets the z component of the vector.
     *
     * This method allows you to change the z value of the vector.
     * The z value is the third number in the vector, representing the depth or the third dimension.
     * By calling this method, you can update the z value to a new number.
     *
     * @param {Number} zVal - The new value for the z component.
     */
    set z(zVal) {
      if (this._values.length > 2) {
        this._values[2] = zVal;
      }
    }

    /**
     * Sets the w component of the vector.
     *
     * This method allows you to change the w value of the vector.
     * The w value is the fourth number in the vector, representing the fourth dimension.
     * By calling this method, you can update the w value to a new number.
     *
     * @param {Number} wVal - The new value for the w component.
     */
    set w(wVal) {
      if (this._values.length > 3) {
        this._values[3] = wVal;
      }
    }

    /**
     * Returns a string representation of a vector.
     *
     * Calling `toString()` is useful for printing vectors to the console while
     * debugging.
     *
     * @return {String} string representation of the vector.
     *
     * @example
     * // META:norender
     * function setup() {
     *   let v = createVector(20, 30);
     *
     *   // Prints 'vector[20, 30, 0]'.
     *   print(v.toString());
     * }
     */
    toString() {
      return `vector[${this._values.join(', ')}]`;
    }

    /**
     * Sets the vector's `x`, `y`, and `z` components.
     *
     * `set()` can use separate numbers, as in `v.set(1, 2, 3)`, a
     * <a href="#/p5.Vector">p5.Vector</a> object, as in `v.set(v2)`, or an
     * array of numbers, as in `v.set([1, 2, 3])`.
     *
     * If a value isn't provided for a component, it will be set to 0. For
     * example, `v.set(4, 5)` sets `v.x` to 4, `v.y` to 5, and `v.z` to 0.
     * Calling `set()` with no arguments, as in `v.set()`, sets all the vector's
     * components to 0.
     *
     * @param {Number} [x] x component of the vector.
     * @param {Number} [y] y component of the vector.
     * @param {Number} [z] z component of the vector.
     * @chainable
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the points.
     *   strokeWeight(5);
     *
     *   // Top left.
     *   let pos = createVector(25, 25);
     *   point(pos);
     *
     *   // Top right.
     *   // set() with numbers.
     *   pos.set(75, 25);
     *   point(pos);
     *
     *   // Bottom right.
     *   // set() with a p5.Vector.
     *   let p2 = createVector(75, 75);
     *   pos.set(p2);
     *   point(pos);
     *
     *   // Bottom left.
     *   // set() with an array.
     *   let arr = [25, 75];
     *   pos.set(arr);
     *   point(pos);
     *
     *   describe('Four black dots arranged in a square on a gray background.');
     * }
     */
    /**
     * @param {p5.Vector|Number[]} value vector to set.
     * @chainable
     */
    set(...args) {
      if (args[0] instanceof Vector) {
        this._values = args[0].values.slice();
      } else if (Array.isArray(args[0])) {
        this._values = args[0].map(arg => arg || 0);
      } else {
        this._values = args.map(arg => arg || 0);
      }
      this.dimensions = this._values.length;
      return this;
    }

    /**
     * Returns a copy of the <a href="#/p5.Vector">p5.Vector</a> object.
     *
     * @return {p5.Vector} copy of the <a href="#/p5.Vector">p5.Vector</a> object.
     *
     * @example
     * function setup() {
     *   createCanvas(100 ,100);
     *
     *   background(200);
     *
     *   // Create a p5.Vector object.
     *   let pos = createVector(50, 50);
     *
     *   // Make a copy.
     *   let pc = pos.copy();
     *
     *   // Draw the point.
     *   strokeWeight(5);
     *   point(pc);
     *
     *   describe('A black point drawn in the middle of a gray square.');
     * }
     */
    copy() {
      if (this.isPInst) {
        return new Vector(this._fromRadians, this._toRadians, ...this._values);
      } else {
        return new Vector(...this._values);
      }
    }

    /**
     * Adds to a vector's components.
     *
     * `add()` can use separate numbers, as in `v.add(1, 2, 3)`,
     * another <a href="#/p5.Vector">p5.Vector</a> object, as in `v.add(v2)`, or
     * an array of numbers, as in `v.add([1, 2, 3])`.
     *
     * If a value isn't provided for a component, it won't change. For
     * example, `v.add(4, 5)` adds 4 to `v.x`, 5 to `v.y`, and 0 to `v.z`.
     * Calling `add()` with no arguments, as in `v.add()`, has no effect.
     *
     * This method supports N-dimensional vectors.
     *
     * The static version of `add()`, as in `p5.Vector.add(v2, v1)`, returns a new
     * <a href="#/p5.Vector">p5.Vector</a> object and doesn't change the
     * originals.
     *
     * @param  {Number|Array} x   x component of the vector to be added or an array of components.
     * @param  {Number} [y] y component of the vector to be added.
     * @param  {Number} [z] z component of the vector to be added.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the points.
     *   strokeWeight(5);
     *
     *   // Top left.
     *   let pos = createVector(25, 25);
     *   point(pos);
     *
     *   // Top right.
     *   // Add numbers.
     *   pos.add(50, 0);
     *   point(pos);
     *
     *   // Bottom right.
     *   // Add a p5.Vector.
     *   let p2 = createVector(0, 50);
     *   pos.add(p2);
     *   point(pos);
     *
     *   // Bottom left.
     *   // Add an array.
     *   let arr = [-50, 0];
     *   pos.add(arr);
     *   point(pos);
     *
     *   describe('Four black dots arranged in a square on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Top left.
     *   let p1 = createVector(25, 25);
     *
     *   // Center.
     *   let p2 = createVector(50, 50);
     *
     *   // Bottom right.
     *   // Add p1 and p2.
     *   let p3 = p5.Vector.add(p1, p2);
     *
     *   // Draw the points.
     *   strokeWeight(5);
     *   point(p1);
     *   point(p2);
     *   point(p3);
     *
     *   describe('Three black dots in a diagonal line from top left to bottom right.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('Three arrows drawn on a gray square. A red arrow extends from the top left corner to the center. A blue arrow extends from the tip of the red arrow. A purple arrow extends from the origin to the tip of the blue arrow.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   let origin = createVector(0, 0);
     *
     *   // Draw the red arrow.
     *   let v1 = createVector(50, 50);
     *   drawArrow(origin, v1, 'red');
     *
     *   // Draw the blue arrow.
     *   let v2 = createVector(-30, 20);
     *   drawArrow(v1, v2, 'blue');
     *
     *   // Purple arrow.
     *   let v3 = p5.Vector.add(v1, v2);
     *   drawArrow(origin, v3, 'purple');
     * }
     *
     * // Draws an arrow between two vectors.
     * function drawArrow(base, vec, myColor) {
     *   push();
     *   stroke(myColor);
     *   strokeWeight(3);
     *   fill(myColor);
     *   translate(base.x, base.y);
     *   line(0, 0, vec.x, vec.y);
     *   rotate(vec.heading());
     *   let arrowSize = 7;
     *   translate(vec.mag() - arrowSize, 0);
     *   triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
     *   pop();
     * }
     */
    /**
     * @param  {p5.Vector|Number[]} value The vector to add
     * @chainable
     */
    add(...args) {
      if (args[0] instanceof Vector) {
        args = args[0].values;
      } else if (Array.isArray(args[0])) {
        args = args[0];
      }
      args.forEach((value, index) => {
        this._values[index] = (this._values[index] || 0) + (value || 0);
      });
      return this;
    }

    /**
     * Performs modulo (remainder) division with a vector's `x`, `y`, and `z`
     * components.
     *
     * `rem()` can use separate numbers, as in `v.rem(1, 2, 3)`,
     * another <a href="#/p5.Vector">p5.Vector</a> object, as in `v.rem(v2)`, or
     * an array of numbers, as in `v.rem([1, 2, 3])`.
     *
     * If only one value is provided, as in `v.rem(2)`, then all the components
     * will be set to their values modulo 2. If two values are provided, as in
     * `v.rem(2, 3)`, then `v.z` won't change. Calling `rem()` with no
     * arguments, as in `v.rem()`, has no effect.
     *
     * The static version of `rem()`, as in `p5.Vector.rem(v2, v1)`, returns a
     * new <a href="#/p5.Vector">p5.Vector</a> object and doesn't change the
     * originals.
     *
     * @param {Number} x x component of divisor vector.
     * @param {Number} y y component of divisor vector.
     * @param {Number} z z component of divisor vector.
     * @chainable
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v = createVector(3, 4, 5);
     *
     *   // Divide numbers.
     *   v.rem(2);
     *
     *   // Prints 'p5.Vector Object : [1, 0, 1]'.
     *   print(v.toString());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v = createVector(3, 4, 5);
     *
     *   // Divide numbers.
     *   v.rem(2, 3);
     *
     *   // Prints 'p5.Vector Object : [1, 1, 5]'.
     *   print(v.toString());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v = createVector(3, 4, 5);
     *
     *   // Divide numbers.
     *   v.rem(2, 3, 4);
     *
     *   // Prints 'p5.Vector Object : [1, 1, 1]'.
     *   print(v.toString());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create p5.Vector objects.
     *   let v1 = createVector(3, 4, 5);
     *   let v2 = createVector(2, 3, 4);
     *
     *   // Divide a p5.Vector.
     *   v1.rem(v2);
     *
     *   // Prints 'p5.Vector Object : [1, 1, 1]'.
     *   print(v1.toString());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v = createVector(3, 4, 5);
     *
     *   // Divide an array.
     *   let arr = [2, 3, 4];
     *   v.rem(arr);
     *
     *   // Prints 'p5.Vector Object : [1, 1, 1]'.
     *   print(v.toString());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create p5.Vector objects.
     *   let v1 = createVector(3, 4, 5);
     *   let v2 = createVector(2, 3, 4);
     *
     *   // Divide without modifying the original vectors.
     *   let v3 = p5.Vector.rem(v1, v2);
     *
     *   // Prints 'p5.Vector Object : [1, 1, 1]'.
     *   print(v3.toString());
     * }
     */
    /**
     * @param {p5.Vector | Number[]}  value  divisor vector.
     * @chainable
     */
    rem(x, y, z) {
      if (x instanceof Vector) {
        if ([x.x, x.y, x.z].every(Number.isFinite)) {
          const xComponent = parseFloat(x.x);
          const yComponent = parseFloat(x.y);
          const zComponent = parseFloat(x.z);
          return calculateRemainder3D.call(
            this,
            xComponent,
            yComponent,
            zComponent
          );
        }
      } else if (Array.isArray(x)) {
        if (x.every(element => Number.isFinite(element))) {
          if (x.length === 2) {
            return calculateRemainder2D.call(this, x[0], x[1]);
          }
          if (x.length === 3) {
            return calculateRemainder3D.call(this, x[0], x[1], x[2]);
          }
        }
      } else if (arguments.length === 1) {
        if (Number.isFinite(arguments[0]) && arguments[0] !== 0) {
          this.x = this.x % arguments[0];
          this.y = this.y % arguments[0];
          this.z = this.z % arguments[0];
          return this;
        }
      } else if (arguments.length === 2) {
        const vectorComponents = [...arguments];
        if (vectorComponents.every(element => Number.isFinite(element))) {
          if (vectorComponents.length === 2) {
            return calculateRemainder2D.call(
              this,
              vectorComponents[0],
              vectorComponents[1]
            );
          }
        }
      } else if (arguments.length === 3) {
        const vectorComponents = [...arguments];
        if (vectorComponents.every(element => Number.isFinite(element))) {
          if (vectorComponents.length === 3) {
            return calculateRemainder3D.call(
              this,
              vectorComponents[0],
              vectorComponents[1],
              vectorComponents[2]
            );
          }
        }
      }
    }

    /**
     * Subtracts from a vector's `x`, `y`, and `z` components.
     *
     * `sub()` can use separate numbers, as in `v.sub(1, 2, 3)`, another
     * <a href="#/p5.Vector">p5.Vector</a> object, as in `v.sub(v2)`, or an array
     * of numbers, as in `v.sub([1, 2, 3])`.
     *
     * If a value isn't provided for a component, it won't change. For
     * example, `v.sub(4, 5)` subtracts 4 from `v.x`, 5 from `v.y`, and 0 from `v.z`.
     * Calling `sub()` with no arguments, as in `v.sub()`, has no effect.
     *
     * The static version of `sub()`, as in `p5.Vector.sub(v2, v1)`, returns a new
     * <a href="#/p5.Vector">p5.Vector</a> object and doesn't change the
     * originals.
     *
     * @param  {Number} x   x component of the vector to subtract.
     * @param  {Number} [y] y component of the vector to subtract.
     * @param  {Number} [z] z component of the vector to subtract.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the points.
     *   strokeWeight(5);
     *
     *   // Bottom right.
     *   let pos = createVector(75, 75);
     *   point(pos);
     *
     *   // Top right.
     *   // Subtract numbers.
     *   pos.sub(0, 50);
     *   point(pos);
     *
     *   // Top left.
     *   // Subtract a p5.Vector.
     *   let p2 = createVector(50, 0);
     *   pos.sub(p2);
     *   point(pos);
     *
     *   // Bottom left.
     *   // Subtract an array.
     *   let arr = [0, -50];
     *   pos.sub(arr);
     *   point(pos);
     *
     *   describe('Four black dots arranged in a square on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create p5.Vector objects.
     *   let p1 = createVector(75, 75);
     *   let p2 = createVector(50, 50);
     *
     *   // Subtract with modifying the original vectors.
     *   let p3 = p5.Vector.sub(p1, p2);
     *
     *   // Draw the points.
     *   strokeWeight(5);
     *   point(p1);
     *   point(p2);
     *   point(p3);
     *
     *   describe('Three black dots in a diagonal line from top left to bottom right.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('Three arrows drawn on a gray square. A red and a blue arrow extend from the top left. A purple arrow extends from the tip of the red arrow to the tip of the blue arrow.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   let origin = createVector(0, 0);
     *
     *   // Draw the red arrow.
     *   let v1 = createVector(50, 50);
     *   drawArrow(origin, v1, 'red');
     *
     *   // Draw the blue arrow.
     *   let v2 = createVector(20, 70);
     *   drawArrow(origin, v2, 'blue');
     *
     *   // Purple arrow.
     *   let v3 = p5.Vector.sub(v2, v1);
     *   drawArrow(v1, v3, 'purple');
     * }
     *
     * // Draws an arrow between two vectors.
     * function drawArrow(base, vec, myColor) {
     *   push();
     *   stroke(myColor);
     *   strokeWeight(3);
     *   fill(myColor);
     *   translate(base.x, base.y);
     *   line(0, 0, vec.x, vec.y);
     *   rotate(vec.heading());
     *   let arrowSize = 7;
     *   translate(vec.mag() - arrowSize, 0);
     *   triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
     *   pop();
     * }
     */
    /**
     * @param  {p5.Vector|Number[]} value the vector to subtract
     * @chainable
     */
    sub(...args) {
      if (args[0] instanceof Vector) {
        args[0].values.forEach((value, index) => {
          this._values[index] -= value || 0;
        });
      } else if (Array.isArray(args[0])) {
        args[0].forEach((value, index) => {
          this._values[index] -= value || 0;
        });
      } else {
        args.forEach((value, index) => {
          this._values[index] -= value || 0;
        });
      }
      return this;
    }

    /**
     * Multiplies a vector's `x`, `y`, and `z` components.
     *
     * `mult()` can use separate numbers, as in `v.mult(1, 2, 3)`, another
     * <a href="#/p5.Vector">p5.Vector</a> object, as in `v.mult(v2)`, or an array
     * of numbers, as in `v.mult([1, 2, 3])`.
     *
     * If only one value is provided, as in `v.mult(2)`, then all the components
     * will be multiplied by 2. If a value isn't provided for a component, it
     * won't change. For example, `v.mult(4, 5)` multiplies `v.x` by, `v.y` by 5,
     * and `v.z` by 1. Calling `mult()` with no arguments, as in `v.mult()`, has
     * no effect.
     *
     * The static version of `mult()`, as in `p5.Vector.mult(v, 2)`, returns a new
     * <a href="#/p5.Vector">p5.Vector</a> object and doesn't change the
     * originals.
     *
     * @param  {Number} n The number to multiply with the vector
     * @chainable
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the points.
     *   strokeWeight(5);
     *
     *   // Top-left.
     *   let p = createVector(25, 25);
     *   point(p);
     *
     *   // Center.
     *   // Multiply all components by 2.
     *   p.mult(2);
     *   point(p);
     *
     *   describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the center.');
     * }
     *
     * @example
     * function setup() {
     *   strokeWeight(5);
     *
     *   // Top-left.
     *   let p = createVector(25, 25);
     *   point(p);
     *
     *   // Bottom-right.
     *   // Multiply p.x * 2 and p.y * 3
     *   p.mult(2, 3);
     *   point(p);
     *
     *   describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the points.
     *   strokeWeight(5);
     *
     *   // Top-left.
     *   let p = createVector(25, 25);
     *   point(p);
     *
     *   // Bottom-right.
     *   // Multiply p.x * 2 and p.y * 3
     *   let arr = [2, 3];
     *   p.mult(arr);
     *   point(p);
     *
     *   describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the points.
     *   strokeWeight(5);
     *
     *   // Top-left.
     *   let p = createVector(25, 25);
     *   point(p);
     *
     *   // Bottom-right.
     *   // Multiply p.x * p2.x and p.y * p2.y
     *   let p2 = createVector(2, 3);
     *   p.mult(p2);
     *   point(p);
     *
     *   describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the points.
     *   strokeWeight(5);
     *
     *   // Top-left.
     *   let p = createVector(25, 25);
     *   point(p);
     *
     *   // Bottom-right.
     *   // Create a new p5.Vector with
     *   // p3.x = p.x * p2.x
     *   // p3.y = p.y * p2.y
     *   let p2 = createVector(2, 3);
     *   let p3 = p5.Vector.mult(p, p2);
     *   point(p3);
     *
     *   describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('Two arrows extending from the top left corner. The blue arrow is twice the length of the red arrow.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   let origin = createVector(0, 0);
     *
     *   // Draw the red arrow.
     *   let v1 = createVector(25, 25);
     *   drawArrow(origin, v1, 'red');
     *
     *   // Draw the blue arrow.
     *   let v2 = p5.Vector.mult(v1, 2);
     *   drawArrow(origin, v2, 'blue');
     * }
     *
     * // Draws an arrow between two vectors.
     * function drawArrow(base, vec, myColor) {
     *   push();
     *   stroke(myColor);
     *   strokeWeight(3);
     *   fill(myColor);
     *   translate(base.x, base.y);
     *   line(0, 0, vec.x, vec.y);
     *   rotate(vec.heading());
     *   let arrowSize = 7;
     *   translate(vec.mag() - arrowSize, 0);
     *   triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
     *   pop();
     * }
     */
    /**
     * @param  {Number} x number to multiply with the x component of the vector.
     * @param  {Number} y number to multiply with the y component of the vector.
     * @param  {Number} [z] number to multiply with the z component of the vector.
     * @chainable
     */
    /**
     * @param  {Number[]} arr array to multiply with the components of the vector.
     * @chainable
     */
    /**
     * @param  {p5.Vector} v vector to multiply with the components of the original vector.
     * @chainable
     */
    mult(...args) {
      if (args.length === 1 && args[0] instanceof Vector) {
        const v = args[0];
        const maxLen = Math.min(this._values.length, v.values.length);
        for (let i = 0; i < maxLen; i++) {
          if (Number.isFinite(v.values[i]) && typeof v.values[i] === 'number') {
            this._values[i] *= v.values[i];
          } else {
            console.warn(
              'p5.Vector.prototype.mult:',
              'v contains components that are either undefined or not finite numbers'
            );
            return this;
          }
        }
      } else if (args.length === 1 && Array.isArray(args[0])) {
        const arr = args[0];
        const maxLen = Math.min(this._values.length, arr.length);
        for (let i = 0; i < maxLen; i++) {
          if (Number.isFinite(arr[i]) && typeof arr[i] === 'number') {
            this._values[i] *= arr[i];
          } else {
            console.warn(
              'p5.Vector.prototype.mult:',
              'arr contains elements that are either undefined or not finite numbers'
            );
            return this;
          }
        }
      } else if (
        args.length === 1 &&
        typeof args[0] === 'number' &&
        Number.isFinite(args[0])
      ) {
        for (let i = 0; i < this._values.length; i++) {
          this._values[i] *= args[0];
        }
      }
      return this;
    }

    /**
     * Divides a vector's `x`, `y`, and `z` components.
     *
     * `div()` can use separate numbers, as in `v.div(1, 2, 3)`, another
     * <a href="#/p5.Vector">p5.Vector</a> object, as in `v.div(v2)`, or an array
     * of numbers, as in `v.div([1, 2, 3])`.
     *
     * If only one value is provided, as in `v.div(2)`, then all the components
     * will be divided by 2. If a value isn't provided for a component, it
     * won't change. For example, `v.div(4, 5)` divides `v.x` by, `v.y` by 5,
     * and `v.z` by 1. Calling `div()` with no arguments, as in `v.div()`, has
     * no effect.
     *
     * The static version of `div()`, as in `p5.Vector.div(v, 2)`, returns a new
     * <a href="#/p5.Vector">p5.Vector</a> object and doesn't change the
     * originals.
     *
     * @param  {Number}    n The number to divide the vector by
     * @chainable
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the points.
     *   strokeWeight(5);
     *
     *   // Center.
     *   let p = createVector(50, 50);
     *   point(p);
     *
     *   // Top-left.
     *   // Divide p.x / 2 and p.y / 2
     *   p.div(2);
     *   point(p);
     *
     *   describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the center.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the points.
     *   strokeWeight(5);
     *
     *   // Bottom-right.
     *   let p = createVector(50, 75);
     *   point(p);
     *
     *   // Top-left.
     *   // Divide p.x / 2 and p.y / 3
     *   p.div(2, 3);
     *   point(p);
     *
     *   describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the points.
     *   strokeWeight(5);
     *
     *   // Bottom-right.
     *   let p = createVector(50, 75);
     *   point(p);
     *
     *   // Top-left.
     *   // Divide p.x / 2 and p.y / 3
     *   let arr = [2, 3];
     *   p.div(arr);
     *   point(p);
     *
     *   describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the points.
     *   strokeWeight(5);
     *
     *   // Bottom-right.
     *   let p = createVector(50, 75);
     *   point(p);
     *
     *   // Top-left.
     *   // Divide p.x / 2 and p.y / 3
     *   let p2 = createVector(2, 3);
     *   p.div(p2);
     *   point(p);
     *
     *   describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the points.
     *   strokeWeight(5);
     *
     *   // Bottom-right.
     *   let p = createVector(50, 75);
     *   point(p);
     *
     *   // Top-left.
     *   // Create a new p5.Vector with
     *   // p3.x = p.x / p2.x
     *   // p3.y = p.y / p2.y
     *   let p2 = createVector(2, 3);
     *   let p3 = p5.Vector.div(p, p2);
     *   point(p3);
     *
     *   describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.');
     * }
     *
     * @example
     * function draw() {
     *   background(200);
     *
     *   let origin = createVector(0, 0);
     *
     *   // Draw the red arrow.
     *   let v1 = createVector(50, 50);
     *   drawArrow(origin, v1, 'red');
     *
     *   // Draw the blue arrow.
     *   let v2 = p5.Vector.div(v1, 2);
     *   drawArrow(origin, v2, 'blue');
     *
     *   describe('Two arrows extending from the top left corner. The blue arrow is half the length of the red arrow.');
     * }
     *
     * // Draws an arrow between two vectors.
     * function drawArrow(base, vec, myColor) {
     *   push();
     *   stroke(myColor);
     *   strokeWeight(3);
     *   fill(myColor);
     *   translate(base.x, base.y);
     *   line(0, 0, vec.x, vec.y);
     *   rotate(vec.heading());
     *   let arrowSize = 7;
     *   translate(vec.mag() - arrowSize, 0);
     *   triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
     *   pop();
     * }
     */
    /**
     * @param  {Number} x number to divide with the x component of the vector.
     * @param  {Number} y number to divide with the y component of the vector.
     * @param  {Number} [z] number to divide with the z component of the vector.
     * @chainable
     */
    /**
     * @param  {Number[]} arr array to divide the components of the vector by.
     * @chainable
     */
    /**
     * @param  {p5.Vector} v vector to divide the components of the original vector by.
     * @chainable
     */
    div(...args) {
      if (args.length === 0) return this;
      if (args.length === 1 && args[0] instanceof Vector) {
        const v = args[0];
        if (
          v._values.every(
            val => Number.isFinite(val) && typeof val === 'number'
          )
        ) {
          if (v._values.some(val => val === 0)) {
            console.warn('p5.Vector.prototype.div:', 'divide by 0');
            return this;
          }
          this._values = this._values.map((val, i) => val / v._values[i]);
        } else {
          console.warn(
            'p5.Vector.prototype.div:',
            'vector contains components that are either undefined or not finite numbers'
          );
        }
        return this;
      }

      if (args.length === 1 && Array.isArray(args[0])) {
        const arr = args[0];
        if (arr.every(val => Number.isFinite(val) && typeof val === 'number')) {
          if (arr.some(val => val === 0)) {
            console.warn('p5.Vector.prototype.div:', 'divide by 0');
            return this;
          }
          this._values = this._values.map((val, i) => val / arr[i]);
        } else {
          console.warn(
            'p5.Vector.prototype.div:',
            'array contains components that are either undefined or not finite numbers'
          );
        }
        return this;
      }

      if (args.every(val => Number.isFinite(val) && typeof val === 'number')) {
        if (args.some(val => val === 0)) {
          console.warn('p5.Vector.prototype.div:', 'divide by 0');
          return this;
        }
        this._values = this._values.map((val, i) => val / args[0]);
      } else {
        console.warn(
          'p5.Vector.prototype.div:',
          'arguments contain components that are either undefined or not finite numbers'
        );
      }

      return this;
    }

    /**
     * Calculates the magnitude (length) of the vector.
     *
     * Use <a href="#/p5/mag">mag()</a> to calculate the magnitude of a 2D vector
     * using components as in `mag(x, y)`.
     *
     * @return {Number} magnitude of the vector.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Vector object.
     *   let p = createVector(30, 40);
     *
     *   // Draw a line from the origin.
     *   line(0, 0, p.x, p.y);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the vector's magnitude.
     *   let m = p.mag();
     *   text(m, p.x, p.y);
     *
     *   describe('A diagonal black line extends from the top left corner of a gray square. The number 50 is written at the end of the line.');
     * }
     */
    mag() {
      return Math.sqrt(this.magSq());
    }

    /**
     * Calculates the magnitude (length) of the vector squared.
     *
     * @return {Number} squared magnitude of the vector.
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Vector object.
     *   let p = createVector(30, 40);
     *
     *   // Draw a line from the origin.
     *   line(0, 0, p.x, p.y);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the vector's magnitude squared.
     *   let m = p.magSq();
     *   text(m, p.x, p.y);
     *
     *   describe('A diagonal black line extends from the top left corner of a gray square. The number 2500 is written at the end of the line.');
     * }
     */
    magSq() {
      return this._values.reduce(
        (sum, component) => sum + component * component,
        0
      );
    }

    /**
     * Calculates the dot product of two vectors.
     *
     * The dot product is a number that describes the overlap between two vectors.
     * Visually, the dot product can be thought of as the "shadow" one vector
     * casts on another. The dot product's magnitude is largest when two vectors
     * point in the same or opposite directions. Its magnitude is 0 when two
     * vectors form a right angle.
     *
     * The version of `dot()` with one parameter interprets it as another
     * <a href="#/p5.Vector">p5.Vector</a> object.
     *
     * The version of `dot()` with multiple parameters interprets them as the
     * `x`, `y`, and `z` components of another vector.
     *
     * The static version of `dot()`, as in `p5.Vector.dot(v1, v2)`, is the same
     * as calling `v1.dot(v2)`.
     *
     * @param  {Number} x   x component of the vector.
     * @param  {Number} [y] y component of the vector.
     * @param  {Number} [z] z component of the vector.
     * @return {Number}     dot product.
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create p5.Vector objects.
     *   let v1 = createVector(3, 4);
     *   let v2 = createVector(3, 0);
     *
     *   // Calculate the dot product.
     *   let dp = v1.dot(v2);
     *
     *   // Prints "9" to the console.
     *   print(dp);
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create p5.Vector objects.
     *   let v1 = createVector(1, 0);
     *   let v2 = createVector(0, 1);
     *
     *   // Calculate the dot product.
     *   let dp = p5.Vector.dot(v1, v2);
     *
     *   // Prints "0" to the console.
     *   print(dp);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('Two arrows drawn on a gray square. A black arrow points to the right and a red arrow follows the mouse. The text "v1 • v2 = something" changes as the mouse moves.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Center.
     *   let v0 = createVector(50, 50);
     *
     *   // Draw the black arrow.
     *   let v1 = createVector(30, 0);
     *   drawArrow(v0, v1, 'black');
     *
     *   // Draw the red arrow.
     *   let v2 = createVector(mouseX - 50, mouseY - 50);
     *   drawArrow(v0, v2, 'red');
     *
     *   // Display the dot product.
     *   let dp = v2.dot(v1);
     *   text(`v2 • v1 = ${dp}`, 10, 20);
     * }
     *
     * // Draws an arrow between two vectors.
     * function drawArrow(base, vec, myColor) {
     *   push();
     *   stroke(myColor);
     *   strokeWeight(3);
     *   fill(myColor);
     *   translate(base.x, base.y);
     *   line(0, 0, vec.x, vec.y);
     *   rotate(vec.heading());
     *   let arrowSize = 7;
     *   translate(vec.mag() - arrowSize, 0);
     *   triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
     *   pop();
     * }
     */
    /**
     * @param  {p5.Vector} v <a href="#/p5.Vector">p5.Vector</a> to be dotted.
     * @return {Number}
     */
    dot(...args) {
      if (args[0] instanceof Vector) {
        return this.dot(...args[0]._values);
      }
      return this._values.reduce((sum, component, index) => {
        return sum + component * (args[index] || 0);
      }, 0);
    }

    /**
     * Calculates the cross product of two vectors.
     *
     * The cross product is a vector that points straight out of the plane created
     * by two vectors. The cross product's magnitude is the area of the parallelogram
     * formed by the original two vectors.
     *
     * The static version of `cross()`, as in `p5.Vector.cross(v1, v2)`, is the same
     * as calling `v1.cross(v2)`.
     *
     * @param  {p5.Vector} v <a href="#/p5.Vector">p5.Vector</a> to be crossed.
     * @return {p5.Vector}   cross product as a <a href="#/p5.Vector">p5.Vector</a>.
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create p5.Vector objects.
     *   let v1 = createVector(1, 0);
     *   let v2 = createVector(3, 4);
     *
     *   // Calculate the cross product.
     *   let cp = v1.cross(v2);
     *
     *   // Prints "p5.Vector Object : [0, 0, 4]" to the console.
     *   print(cp.toString());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create p5.Vector objects.
     *   let v1 = createVector(1, 0);
     *   let v2 = createVector(3, 4);
     *
     *   // Calculate the cross product.
     *   let cp = p5.Vector.cross(v1, v2);
     *
     *   // Prints "p5.Vector Object : [0, 0, 4]" to the console.
     *   print(cp.toString());
     * }
     */
    cross(v) {
      const x = this.y * v.z - this.z * v.y;
      const y = this.z * v.x - this.x * v.z;
      const z = this.x * v.y - this.y * v.x;
      if (this.isPInst) {
        return new Vector(this._fromRadians, this._toRadians, x, y, z);
      } else {
        return new Vector(x, y, z);
      }
    }

    /**
     * Calculates the distance between two points represented by vectors.
     *
     * A point's coordinates can be represented by the components of a vector
     * that extends from the origin to the point.
     *
     * The static version of `dist()`, as in `p5.Vector.dist(v1, v2)`, is the same
     * as calling `v1.dist(v2)`.
     *
     * Use <a href="#/p5/dist">dist()</a> to calculate the distance between points
     * using coordinates as in `dist(x1, y1, x2, y2)`.
     *
     * @submodule p5.Vector
     * @param  {p5.Vector} v x, y, and z coordinates of a <a href="#/p5.Vector">p5.Vector</a>.
     * @return {Number}      distance.
     *
     * @example
     * // META:norender
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create p5.Vector objects.
     *   let v1 = createVector(1, 0);
     *   let v2 = createVector(0, 1);
     *
     *   // Calculate the distance between them.
     *   let d = v1.dist(v2);
     *
     *   // Prints "1.414..." to the console.
     *   print(d);
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create p5.Vector objects.
     *   let v1 = createVector(1, 0);
     *   let v2 = createVector(0, 1);
     *
     *   // Calculate the distance between them.
     *   let d = p5.Vector.dist(v1, v2);
     *
     *   // Prints "1.414..." to the console.
     *   print(d);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('Three arrows drawn on a gray square. A red and a blue arrow extend from the top left. A purple arrow extends from the tip of the red arrow to the tip of the blue arrow. The number 36 is written in black near the purple arrow.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   let origin = createVector(0, 0);
     *
     *   // Draw the red arrow.
     *   let v1 = createVector(50, 50);
     *   drawArrow(origin, v1, 'red');
     *
     *   // Draw the blue arrow.
     *   let v2 = createVector(20, 70);
     *   drawArrow(origin, v2, 'blue');
     *
     *   // Purple arrow.
     *   let v3 = p5.Vector.sub(v2, v1);
     *   drawArrow(v1, v3, 'purple');
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *
     *   // Display the magnitude. The same as floor(v3.mag());
     *   let m = floor(p5.Vector.dist(v1, v2));
     *   text(m, 50, 75);
     * }
     *
     * // Draws an arrow between two vectors.
     * function drawArrow(base, vec, myColor) {
     *   push();
     *   stroke(myColor);
     *   strokeWeight(3);
     *   fill(myColor);
     *   translate(base.x, base.y);
     *   line(0, 0, vec.x, vec.y);
     *   rotate(vec.heading());
     *   let arrowSize = 7;
     *   translate(vec.mag() - arrowSize, 0);
     *   triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
     *   pop();
     * }
     */
    dist(v) {
      return v.copy().sub(this).mag();
    }

    /**
     * Scales the components of a <a href="#/p5.Vector">p5.Vector</a> object so
     * that its magnitude is 1.
     *
     * The static version of `normalize()`,  as in `p5.Vector.normalize(v)`,
     * returns a new <a href="#/p5.Vector">p5.Vector</a> object and doesn't change
     * the original.
     *
     * @return {p5.Vector} normalized <a href="#/p5.Vector">p5.Vector</a>.
     *
     * @example
     * // META:norender
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Vector.
     *   let v = createVector(10, 20, 2);
     *
     *   // Normalize.
     *   v.normalize();
     *
     *   // Prints "p5.Vector Object : [0.445..., 0.890..., 0.089...]" to the console.
     *   print(v.toString());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Vector.
     *   let v0 = createVector(10, 20, 2);
     *
     *   // Create a normalized copy.
     *   let v1 = p5.Vector.normalize(v0);
     *
     *   // Prints "p5.Vector Object : [10, 20, 2]" to the console.
     *   print(v0.toString());
     *   // Prints "p5.Vector Object : [0.445..., 0.890..., 0.089...]" to the console.
     *   print(v1.toString());
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe("A red and blue arrow extend from the center of a circle. Both arrows follow the mouse, but the blue arrow's length is fixed to the circle's radius.");
     * }
     *
     * function draw() {
     *   background(240);
     *
     *   // Vector to the center.
     *   let v0 = createVector(50, 50);
     *
     *   // Vector from the center to the mouse.
     *   let v1 = createVector(mouseX - 50, mouseY - 50);
     *
     *   // Circle's radius.
     *   let r = 25;
     *
     *   // Draw the red arrow.
     *   drawArrow(v0, v1, 'red');
     *
     *   // Draw the blue arrow.
     *   v1.normalize();
     *   drawArrow(v0, v1.mult(r), 'blue');
     *
     *   // Draw the circle.
     *   noFill();
     *   circle(50, 50, r * 2);
     * }
     *
     * // Draws an arrow between two vectors.
     * function drawArrow(base, vec, myColor) {
     *   push();
     *   stroke(myColor);
     *   strokeWeight(3);
     *   fill(myColor);
     *   translate(base.x, base.y);
     *   line(0, 0, vec.x, vec.y);
     *   rotate(vec.heading());
     *   let arrowSize = 7;
     *   translate(vec.mag() - arrowSize, 0);
     *   triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
     *   pop();
     * }
     */
    normalize() {
      const len = this.mag();
      // here we multiply by the reciprocal instead of calling 'div()'
      // since div duplicates this zero check.
      if (len !== 0) this.mult(1 / len);
      return this;
    }

    /**
     * Limits a vector's magnitude to a maximum value.
     *
     * The static version of `limit()`, as in `p5.Vector.limit(v, 5)`, returns a
     * new <a href="#/p5.Vector">p5.Vector</a> object and doesn't change the
     * original.
     *
     * @param  {Number}    max maximum magnitude for the vector.
     * @chainable
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v = createVector(10, 20, 2);
     *
     *   // Limit its magnitude.
     *   v.limit(5);
     *
     *   // Prints "p5.Vector Object : [2.227..., 4.454..., 0.445...]" to the console.
     *   print(v.toString());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v0 = createVector(10, 20, 2);
     *
     *   // Create a copy an limit its magintude.
     *   let v1 = p5.Vector.limit(v0, 5);
     *
     *   // Prints "p5.Vector Object : [2.227..., 4.454..., 0.445...]" to the console.
     *   print(v1.toString());
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe("A red and blue arrow extend from the center of a circle. Both arrows follow the mouse, but the blue arrow never crosses the circle's edge.");
     * }
     * function draw() {
     *   background(240);
     *
     *   // Vector to the center.
     *   let v0 = createVector(50, 50);
     *
     *   // Vector from the center to the mouse.
     *   let v1 = createVector(mouseX - 50, mouseY - 50);
     *
     *   // Circle's radius.
     *   let r = 25;
     *
     *   // Draw the red arrow.
     *   drawArrow(v0, v1, 'red');
     *
     *   // Draw the blue arrow.
     *   drawArrow(v0, v1.limit(r), 'blue');
     *
     *   // Draw the circle.
     *   noFill();
     *   circle(50, 50, r * 2);
     * }
     *
     * // Draws an arrow between two vectors.
     * function drawArrow(base, vec, myColor) {
     *   push();
     *   stroke(myColor);
     *   strokeWeight(3);
     *   fill(myColor);
     *   translate(base.x, base.y);
     *   line(0, 0, vec.x, vec.y);
     *   rotate(vec.heading());
     *   let arrowSize = 7;
     *   translate(vec.mag() - arrowSize, 0);
     *   triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
     *   pop();
     * }
     */
    limit(max) {
      const mSq = this.magSq();
      if (mSq > max * max) {
        this.div(Math.sqrt(mSq)) //normalize it
          .mult(max);
      }
      return this;
    }

    /**
     * Sets a vector's magnitude to a given value.
     *
     * The static version of `setMag()`, as in `p5.Vector.setMag(v, 10)`, returns
     * a new <a href="#/p5.Vector">p5.Vector</a> object and doesn't change the
     * original.
     *
     * @param  {Number}    len new length for this vector.
     * @chainable
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v = createVector(3, 4, 0);
     *
     *   // Prints "5" to the console.
     *   print(v.mag());
     *
     *   // Set its magnitude to 10.
     *   v.setMag(10);
     *
     *   // Prints "p5.Vector Object : [6, 8, 0]" to the console.
     *   print(v.toString());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v0 = createVector(3, 4, 0);
     *
     *   // Create a copy with a magnitude of 10.
     *   let v1 = p5.Vector.setMag(v0, 10);
     *
     *   // Prints "5" to the console.
     *   print(v0.mag());
     *
     *   // Prints "p5.Vector Object : [6, 8, 0]" to the console.
     *   print(v1.toString());
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('Two arrows extend from the top left corner of a square toward its center. The red arrow reaches the center and the blue arrow only extends part of the way.');
     * }
     *
     * function draw() {
     *   background(240);
     *
     *   let origin = createVector(0, 0);
     *   let v = createVector(50, 50);
     *
     *   // Draw the red arrow.
     *   drawArrow(origin, v, 'red');
     *
     *   // Set v's magnitude to 30.
     *   v.setMag(30);
     *
     *   // Draw the blue arrow.
     *   drawArrow(origin, v, 'blue');
     * }
     *
     * // Draws an arrow between two vectors.
     * function drawArrow(base, vec, myColor) {
     *   push();
     *   stroke(myColor);
     *   strokeWeight(3);
     *   fill(myColor);
     *   translate(base.x, base.y);
     *   line(0, 0, vec.x, vec.y);
     *   rotate(vec.heading());
     *   let arrowSize = 7;
     *   translate(vec.mag() - arrowSize, 0);
     *   triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
     *   pop();
     * }
     */
    setMag(n) {
      return this.normalize().mult(n);
    }

    /**
     * Calculates the angle a 2D vector makes with the positive x-axis.
     *
     * By convention, the positive x-axis has an angle of 0. Angles increase in
     * the clockwise direction.
     *
     * If the vector was created with
     * <a href="#/p5/createVector">createVector()</a>, `heading()` returns angles
     * in the units of the current <a href="#/p5/angleMode">angleMode()</a>.
     *
     * The static version of `heading()`, as in `p5.Vector.heading(v)`, works the
     * same way.
     *
     * @return {Number} angle of rotation.
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v = createVector(1, 1);
     *
     *   // Prints "0.785..." to the console.
     *   print(v.heading());
     *
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   // Prints "45" to the console.
     *   print(v.heading());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v = createVector(1, 1);
     *
     *   // Prints "0.785..." to the console.
     *   print(p5.Vector.heading(v));
     *
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   // Prints "45" to the console.
     *   print(p5.Vector.heading(v));
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A black arrow extends from the top left of a square to its center. The text "Radians: 0.79" and "Degrees: 45" is written near the tip of the arrow.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   let origin = createVector(0, 0);
     *   let v = createVector(50, 50);
     *
     *   // Draw the black arrow.
     *   drawArrow(origin, v, 'black');
     *
     *   // Use radians.
     *   angleMode(RADIANS);
     *
     *   // Display the heading in radians.
     *   let h = round(v.heading(), 2);
     *   text(`Radians: ${h}`, 20, 70);
     *
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   // Display the heading in degrees.
     *   h = v.heading();
     *   text(`Degrees: ${h}`, 20, 85);
     * }
     *
     * // Draws an arrow between two vectors.
     * function drawArrow(base, vec, myColor) {
     *   push();
     *   stroke(myColor);
     *   strokeWeight(3);
     *   fill(myColor);
     *   translate(base.x, base.y);
     *   line(0, 0, vec.x, vec.y);
     *   rotate(vec.heading());
     *   let arrowSize = 7;
     *   translate(vec.mag() - arrowSize, 0);
     *   triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
     *   pop();
     * }
     */
    heading() {
      const h = Math.atan2(this.y, this.x);
      if (this.isPInst) return this._fromRadians(h);
      return h;
    }

    /**
     * Rotates a 2D vector to a specific angle without changing its magnitude.
     *
     * By convention, the positive x-axis has an angle of 0. Angles increase in
     * the clockwise direction.
     *
     * If the vector was created with
     * <a href="#/p5/createVector">createVector()</a>, `setHeading()` uses
     * the units of the current <a href="#/p5/angleMode">angleMode()</a>.
     *
     * @param  {Number}    angle angle of rotation.
     * @chainable
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v = createVector(0, 1);
     *
     *   // Prints "1.570..." to the console.
     *   print(v.heading());
     *
     *   // Point to the left.
     *   v.setHeading(PI);
     *
     *   // Prints "3.141..." to the console.
     *   print(v.heading());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   // Create a p5.Vector object.
     *   let v = createVector(0, 1);
     *
     *   // Prints "90" to the console.
     *   print(v.heading());
     *
     *   // Point to the left.
     *   v.setHeading(180);
     *
     *   // Prints "180" to the console.
     *   print(v.heading());
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('Two arrows extend from the center of a gray square. The red arrow points to the right and the blue arrow points down.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Create p5.Vector objects.
     *   let v0 = createVector(50, 50);
     *   let v1 = createVector(30, 0);
     *
     *   // Draw the red arrow.
     *   drawArrow(v0, v1, 'red');
     *
     *   // Point down.
     *   v1.setHeading(HALF_PI);
     *
     *   // Draw the blue arrow.
     *   drawArrow(v0, v1, 'blue');
     * }
     *
     * // Draws an arrow between two vectors.
     * function drawArrow(base, vec, myColor) {
     *   push();
     *   stroke(myColor);
     *   strokeWeight(3);
     *   fill(myColor);
     *   translate(base.x, base.y);
     *   line(0, 0, vec.x, vec.y);
     *   rotate(vec.heading());
     *   let arrowSize = 7;
     *   translate(vec.mag() - arrowSize, 0);
     *   triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
     *   pop();
     * }
     */
    setHeading(a) {
      if (this.isPInst) a = this._toRadians(a);
      let m = this.mag();
      this.x = m * Math.cos(a);
      this.y = m * Math.sin(a);
      return this;
    }

    /**
     * Rotates a 2D vector by an angle without changing its magnitude.
     *
     * By convention, the positive x-axis has an angle of 0. Angles increase in
     * the clockwise direction.
     *
     * If the vector was created with
     * <a href="#/p5/createVector">createVector()</a>, `rotate()` uses
     * the units of the current <a href="#/p5/angleMode">angleMode()</a>.
     *
     * The static version of `rotate()`, as in `p5.Vector.rotate(v, PI)`,
     * returns a new <a href="#/p5.Vector">p5.Vector</a> object and doesn't change
     * the original.
     *
     * @param  {Number}    angle angle of rotation.
     * @chainable
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v = createVector(1, 0);
     *
     *   // Prints "p5.Vector Object : [1, 0, 0]" to the console.
     *   print(v.toString());
     *
     *   // Rotate a quarter turn.
     *   v.rotate(HALF_PI);
     *
     *   // Prints "p5.Vector Object : [0, 1, 0]" to the console.
     *   print(v.toString());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   // Create a p5.Vector object.
     *   let v = createVector(1, 0);
     *
     *   // Prints "p5.Vector Object : [1, 0, 0]" to the console.
     *   print(v.toString());
     *
     *   // Rotate a quarter turn.
     *   v.rotate(90);
     *
     *   // Prints "p5.Vector Object : [0, 1, 0]" to the console.
     *   print(v.toString());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v0 = createVector(1, 0);
     *
     *   // Create a rotated copy.
     *   let v1 = p5.Vector.rotate(v0, HALF_PI);
     *
     *   // Prints "p5.Vector Object : [1, 0, 0]" to the console.
     *   print(v0.toString());
     *   // Prints "p5.Vector Object : [0, 1, 0]" to the console.
     *   print(v1.toString());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   // Create a p5.Vector object.
     *   let v0 = createVector(1, 0);
     *
     *   // Create a rotated copy.
     *   let v1 = p5.Vector.rotate(v0, 90);
     *
     *   // Prints "p5.Vector Object : [1, 0, 0]" to the console.
     *   print(v0.toString());
     *
     *   // Prints "p5.Vector Object : [0, 1, 0]" to the console.
     *   print(v1.toString());
     * }
     *
     * @example
     * let v0;
     * let v1;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create p5.Vector objects.
     *   v0 = createVector(50, 50);
     *   v1 = createVector(30, 0);
     *
     *   describe('A black arrow extends from the center of a gray square. The arrow rotates clockwise.');
     * }
     *
     * function draw() {
     *   background(240);
     *
     *   // Rotate v1.
     *   v1.rotate(0.01);
     *
     *   // Draw the black arrow.
     *   drawArrow(v0, v1, 'black');
     * }
     *
     * // Draws an arrow between two vectors.
     * function drawArrow(base, vec, myColor) {
     *   push();
     *   stroke(myColor);
     *   strokeWeight(3);
     *   fill(myColor);
     *   translate(base.x, base.y);
     *   line(0, 0, vec.x, vec.y);
     *   rotate(vec.heading());
     *   let arrowSize = 7;
     *   translate(vec.mag() - arrowSize, 0);
     *   triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
     *   pop();
     * }
     */
    rotate(a) {
      let newHeading = this.heading() + a;
      if (this.isPInst) newHeading = this._toRadians(newHeading);
      const mag = this.mag();
      this.x = Math.cos(newHeading) * mag;
      this.y = Math.sin(newHeading) * mag;
      return this;
    }

    /**
     * Calculates the angle between two vectors.
     *
     * The angles returned are signed, which means that
     * `v1.angleBetween(v2) === -v2.angleBetween(v1)`.
     *
     * If the vector was created with
     * <a href="#/p5/createVector">createVector()</a>, `angleBetween()` returns
     * angles in the units of the current
     * <a href="#/p5/angleMode">angleMode()</a>.
     *
     * @param  {p5.Vector}    value x, y, and z components of a <a href="#/p5.Vector">p5.Vector</a>.
     * @return {Number}       angle between the vectors.
     * @example
     * // META:norender
     * function setup() {
     *   // Create p5.Vector objects.
     *   let v0 = createVector(1, 0);
     *   let v1 = createVector(0, 1);
     *
     *   // Prints "1.570..." to the console.
     *   print(v0.angleBetween(v1));
     *
     *   // Prints "-1.570..." to the console.
     *   print(v1.angleBetween(v0));
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Use degrees.
     *   angleMode(DEGREES);
     *   // Create p5.Vector objects.
     *   let v0 = createVector(1, 0);
     *   let v1 = createVector(0, 1);
     *
     *   // Prints "90" to the console.
     *   print(v0.angleBetween(v1));
     *
     *   // Prints "-90" to the console.
     *   print(v1.angleBetween(v0));
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create p5.Vector objects.
     *   let v0 = createVector(1, 0);
     *   let v1 = createVector(0, 1);
     *
     *   // Prints "1.570..." to the console.
     *   print(p5.Vector.angleBetween(v0, v1));
     *
     *   // Prints "-1.570..." to the console.
     *   print(p5.Vector.angleBetween(v1, v0));
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   // Create p5.Vector objects.
     *   let v0 = createVector(1, 0);
     *   let v1 = createVector(0, 1);
     *
     *   // Prints "90" to the console.
     *   print(p5.Vector.angleBetween(v0, v1));
     *
     *   // Prints "-90" to the console.
     *   print(p5.Vector.angleBetween(v1, v0));
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('Two arrows extend from the center of a gray square. A red arrow points to the right and a blue arrow points down. The text "Radians: 1.57" and "Degrees: 90" is written above the arrows.');
     * }
     * function draw() {
     *   background(200);
     *
     *   // Create p5.Vector objects.
     *   let v0 = createVector(50, 50);
     *   let v1 = createVector(30, 0);
     *   let v2 = createVector(0, 30);
     *
     *   // Draw the red arrow.
     *   drawArrow(v0, v1, 'red');
     *
     *   // Draw the blue arrow.
     *   drawArrow(v0, v2, 'blue');
     *
     *   // Use radians.
     *   angleMode(RADIANS);
     *
     *   // Display the angle in radians.
     *   let angle = round(v1.angleBetween(v2), 2);
     *   text(`Radians: ${angle}`, 20, 20);
     *
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   // Display the angle in degrees.
     *   angle = round(v1.angleBetween(v2), 2);
     *   text(`Degrees: ${angle}`, 20, 35);
     * }
     *
     * // Draws an arrow between two vectors.
     * function drawArrow(base, vec, myColor) {
     *   push();
     *   stroke(myColor);
     *   strokeWeight(3);
     *   fill(myColor);
     *   translate(base.x, base.y);
     *   line(0, 0, vec.x, vec.y);
     *   rotate(vec.heading());
     *   let arrowSize = 7;
     *   translate(vec.mag() - arrowSize, 0);
     *   triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
     *   pop();
     * }
     */
    angleBetween(v) {
      const magSqMult = this.magSq() * v.magSq();
      // Returns NaN if either vector is the zero vector.
      if (magSqMult === 0) {
        return NaN;
      }
      const u = this.cross(v);
      // The dot product computes the cos value, and the cross product computes
      // the sin value. Find the angle based on them. In addition, in the case of
      // 2D vectors, a sign is added according to the direction of the vector.
      let angle = Math.atan2(u.mag(), this.dot(v)) * Math.sign(u.z || 1);
      if (this.isPInst) {
        angle = this._fromRadians(angle);
      }
      return angle;
    }

    /**
     * Calculates new `x`, `y`, and `z` components that are proportionally the
     * same distance between two vectors.
     *
     * The `amt` parameter is the amount to interpolate between the old vector and
     * the new vector. 0.0 keeps all components equal to the old vector's, 0.5 is
     * halfway between, and 1.0 sets all components equal to the new vector's.
     *
     * The static version of `lerp()`, as in `p5.Vector.lerp(v0, v1, 0.5)`,
     * returns a new <a href="#/p5.Vector">p5.Vector</a> object and doesn't change
     * the original.
     *
     * @param  {Number}    x   x component.
     * @param  {Number}    y   y component.
     * @param  {Number}    z   z component.
     * @param  {Number}    amt amount of interpolation between 0.0 (old vector)
     *                         and 1.0 (new vector). 0.5 is halfway between.
     * @chainable
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v0 = createVector(1, 1, 1);
     *   let v1 = createVector(3, 3, 3);
     *
     *   // Interpolate.
     *   v0.lerp(v1, 0.5);
     *
     *   // Prints "p5.Vector Object : [2, 2, 2]" to the console.
     *   print(v0.toString());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v = createVector(1, 1, 1);
     *
     *   // Interpolate.
     *   v.lerp(3, 3, 3, 0.5);
     *
     *   // Prints "p5.Vector Object : [2, 2, 2]" to the console.
     *   print(v.toString());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create p5.Vector objects.
     *   let v0 = createVector(1, 1, 1);
     *   let v1 = createVector(3, 3, 3);
     *
     *   // Interpolate.
     *   let v2 = p5.Vector.lerp(v0, v1, 0.5);
     *
     *   // Prints "p5.Vector Object : [2, 2, 2]" to the console.
     *   print(v2.toString());
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('Three arrows extend from the center of a gray square. A red arrow points to the right, a blue arrow points down, and a purple arrow points to the bottom right.');
     * }
     * function draw() {
     *   background(200);
     *
     *   // Create p5.Vector objects.
     *   let v0 = createVector(50, 50);
     *   let v1 = createVector(30, 0);
     *   let v2 = createVector(0, 30);
     *
     *   // Interpolate.
     *   let v3 = p5.Vector.lerp(v1, v2, 0.5);
     *
     *   // Draw the red arrow.
     *   drawArrow(v0, v1, 'red');
     *
     *   // Draw the blue arrow.
     *   drawArrow(v0, v2, 'blue');
     *
     *   // Draw the purple arrow.
     *   drawArrow(v0, v3, 'purple');
     * }
     *
     * // Draws an arrow between two vectors.
     * function drawArrow(base, vec, myColor) {
     *   push();
     *   stroke(myColor);
     *   strokeWeight(3);
     *   fill(myColor);
     *   translate(base.x, base.y);
     *   line(0, 0, vec.x, vec.y);
     *   rotate(vec.heading());
     *   let arrowSize = 7;
     *   translate(vec.mag() - arrowSize, 0);
     *   triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
     *   pop();
     * }
     */
    /**
     * @param  {p5.Vector} v  <a href="#/p5.Vector">p5.Vector</a> to lerp toward.
     * @param  {Number}    amt
     * @chainable
     */
    lerp(x, y, z, amt) {
      if (x instanceof Vector) {
        return this.lerp(x.x, x.y, x.z, y);
      }
      this.x += (x - this.x) * amt || 0;
      this.y += (y - this.y) * amt || 0;
      this.z += (z - this.z) * amt || 0;
      return this;
    }

    /**
     * Calculates a new heading and magnitude that are between two vectors.
     *
     * The `amt` parameter is the amount to interpolate between the old vector and
     * the new vector. 0.0 keeps the heading and magnitude equal to the old
     * vector's, 0.5 sets them halfway between, and 1.0 sets the heading and
     * magnitude equal to the new vector's.
     *
     * `slerp()` differs from <a href="#/p5.Vector/lerp">lerp()</a> because
     * it interpolates magnitude. Calling `v0.slerp(v1, 0.5)` sets `v0`'s
     * magnitude to a value halfway between its original magnitude and `v1`'s.
     * Calling `v0.lerp(v1, 0.5)` makes no such guarantee.
     *
     * The static version of `slerp()`, as in `p5.Vector.slerp(v0, v1, 0.5)`,
     * returns a new <a href="#/p5.Vector">p5.Vector</a> object and doesn't change
     * the original.
     *
     * @param {p5.Vector} v <a href="#/p5.Vector">p5.Vector</a> to slerp toward.
     * @param {Number} amt  amount of interpolation between 0.0 (old vector)
     *                      and 1.0 (new vector). 0.5 is halfway between.
     * @return {p5.Vector}
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v0 = createVector(3, 0);
     *
     *   // Prints "3" to the console.
     *   print(v0.mag());
     *
     *   // Prints "0" to the console.
     *   print(v0.heading());
     *
     *   // Create a p5.Vector object.
     *   let v1 = createVector(0, 1);
     *
     *   // Prints "1" to the console.
     *   print(v1.mag());
     *
     *   // Prints "1.570..." to the console.
     *   print(v1.heading());
     *
     *   // Interpolate halfway between v0 and v1.
     *   v0.slerp(v1, 0.5);
     *
     *   // Prints "2" to the console.
     *   print(v0.mag());
     *
     *   // Prints "0.785..." to the console.
     *   print(v0.heading());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v0 = createVector(3, 0);
     *
     *   // Prints "3" to the console.
     *   print(v0.mag());
     *
     *   // Prints "0" to the console.
     *   print(v0.heading());
     *
     *   // Create a p5.Vector object.
     *   let v1 = createVector(0, 1);
     *
     *   // Prints "1" to the console.
     *   print(v1.mag());
     *
     *   // Prints "1.570..." to the console.
     *   print(v1.heading());
     *
     *   // Create a p5.Vector that's halfway between v0 and v1.
     *   let v3 = p5.Vector.slerp(v0, v1, 0.5);
     *
     *   // Prints "2" to the console.
     *   print(v3.mag());
     *
     *   // Prints "0.785..." to the console.
     *   print(v3.heading());
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('Three arrows extend from the center of a gray square. A red arrow points to the right, a blue arrow points to the left, and a purple arrow points down.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Create p5.Vector objects.
     *   let v0 = createVector(50, 50);
     *   let v1 = createVector(20, 0);
     *   let v2 = createVector(-40, 0);
     *
     *   // Create a p5.Vector that's halfway between v1 and v2.
     *   let v3 = p5.Vector.slerp(v1, v2, 0.5);
     *
     *   // Draw the red arrow.
     *   drawArrow(v0, v1, 'red');
     *
     *   // Draw the blue arrow.
     *   drawArrow(v0, v2, 'blue');
     *
     *   // Draw the purple arrow.
     *   drawArrow(v0, v3, 'purple');
     * }
     *
     * // Draws an arrow between two vectors.
     * function drawArrow(base, vec, myColor) {
     *   push();
     *   stroke(myColor);
     *   strokeWeight(3);
     *   fill(myColor);
     *   translate(base.x, base.y);
     *   line(0, 0, vec.x, vec.y);
     *   rotate(vec.heading());
     *   let arrowSize = 7;
     *   translate(vec.mag() - arrowSize, 0);
     *   triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
     *   pop();
     * }
     */
    slerp(v, amt) {
      // edge cases.
      if (amt === 0) {
        return this;
      }
      if (amt === 1) {
        return this.set(v);
      }

      // calculate magnitudes
      const selfMag = this.mag();
      const vMag = v.mag();
      const magmag = selfMag * vMag;
      // if either is a zero vector, linearly interpolate by these vectors
      if (magmag === 0) {
        this.mult(1 - amt).add(v.x * amt, v.y * amt, v.z * amt);
        return this;
      }
      // the cross product of 'this' and 'v' is the axis of rotation
      const axis = this.cross(v);
      const axisMag = axis.mag();
      // Calculates the angle between 'this' and 'v'
      const theta = Math.atan2(axisMag, this.dot(v));

      // However, if the norm of axis is 0, normalization cannot be performed,
      // so we will divide the cases
      if (axisMag > 0) {
        axis.x /= axisMag;
        axis.y /= axisMag;
        axis.z /= axisMag;
      } else if (theta < Math.PI * 0.5) {
        // if the norm is 0 and the angle is less than PI/2,
        // the angle is very close to 0, so do linear interpolation.
        this.mult(1 - amt).add(v.x * amt, v.y * amt, v.z * amt);
        return this;
      } else {
        // If the norm is 0 and the angle is more than PI/2, the angle is
        // very close to PI.
        // In this case v can be regarded as '-this', so take any vector
        // that is orthogonal to 'this' and use that as the axis.
        if (this.z === 0 && v.z === 0) {
          // if both this and v are 2D vectors, use (0,0,1)
          // this makes the result also a 2D vector.
          axis.set(0, 0, 1);
        } else if (this.x !== 0) {
          // if the x components is not 0, use (y, -x, 0)
          axis.set(this.y, -this.x, 0).normalize();
        } else {
          // if the x components is 0, use (1,0,0)
          axis.set(1, 0, 0);
        }
      }

      // Since 'axis' is a unit vector, ey is a vector of the same length as 'this'.
      const ey = axis.cross(this);
      // interpolate the length with 'this' and 'v'.
      const lerpedMagFactor = 1 - amt + (amt * vMag) / selfMag;
      // imagine a situation where 'axis', 'this', and 'ey' are pointing
      // along the z, x, and y axes, respectively.
      // rotates 'this' around 'axis' by amt * theta towards 'ey'.
      const cosMultiplier = lerpedMagFactor * Math.cos(amt * theta);
      const sinMultiplier = lerpedMagFactor * Math.sin(amt * theta);
      // then, calculate 'result'.
      this.x = this.x * cosMultiplier + ey.x * sinMultiplier;
      this.y = this.y * cosMultiplier + ey.y * sinMultiplier;
      this.z = this.z * cosMultiplier + ey.z * sinMultiplier;

      return this;
    }

    /**
     * Reflects a vector about a line in 2D or a plane in 3D.
     *
     * The orientation of the line or plane is described by a normal vector that
     * points away from the shape.
     *
     * The static version of `reflect()`, as in `p5.Vector.reflect(v, n)`,
     * returns a new <a href="#/p5.Vector">p5.Vector</a> object and doesn't change
     * the original.
     *
     * @param  {p5.Vector} surfaceNormal  <a href="#/p5.Vector">p5.Vector</a>
     *                                    to reflect about.
     * @chainable
     * @example
     * // META:norender
     * function setup() {
     *   // Create a normal vector.
     *   let n = createVector(0, 1);
     *   // Create a vector to reflect.
     *   let v = createVector(4, 6);
     *
     *   // Reflect v about n.
     *   v.reflect(n);
     *
     *   // Prints "p5.Vector Object : [4, -6, 0]" to the console.
     *   print(v.toString());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a normal vector.
     *   let n = createVector(0, 1);
     *
     *   // Create a vector to reflect.
     *   let v0 = createVector(4, 6);
     *
     *   // Create a reflected vector.
     *   let v1 = p5.Vector.reflect(v0, n);
     *
     *   // Prints "p5.Vector Object : [4, -6, 0]" to the console.
     *   print(v1.toString());
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('Three arrows extend from the center of a gray square with a vertical line down its middle. A black arrow points to the right, a blue arrow points to the bottom left, and a red arrow points to the bottom right.');
     * }
     * function draw() {
     *   background(200);
     *
     *   // Draw a vertical line.
     *   line(50, 0, 50, 100);
     *
     *   // Create a normal vector.
     *   let n = createVector(1, 0);
     *
     *   // Center.
     *   let v0 = createVector(50, 50);
     *
     *   // Create a vector to reflect.
     *   let v1 = createVector(30, 40);
     *
     *   // Create a reflected vector.
     *   let v2 = p5.Vector.reflect(v1, n);
     *
     *   // Scale the normal vector for drawing.
     *   n.setMag(30);
     *
     *   // Draw the black arrow.
     *   drawArrow(v0, n, 'black');
     *
     *   // Draw the red arrow.
     *   drawArrow(v0, v1, 'red');
     *
     *   // Draw the blue arrow.
     *   drawArrow(v0, v2, 'blue');
     * }
     *
     * // Draws an arrow between two vectors.
     * function drawArrow(base, vec, myColor) {
     *   push();
     *   stroke(myColor);
     *   strokeWeight(3);
     *   fill(myColor);
     *   translate(base.x, base.y);
     *   line(0, 0, vec.x, vec.y);
     *   rotate(vec.heading());
     *   let arrowSize = 7;
     *   translate(vec.mag() - arrowSize, 0);
     *   triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
     *   pop();
     * }
     */
    reflect(surfaceNormal) {
      const surfaceNormalCopy = Vector.normalize(surfaceNormal);
      return this.sub(surfaceNormalCopy.mult(2 * this.dot(surfaceNormalCopy)));
    }

    /**
     * Returns the vector's components as an array of numbers.
     *
     * @return {Number[]} array with the vector's components.
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v = createVector(20, 30);
     *
     *   // Prints "[20, 30, 0]" to the console.
     *   print(v.array());
     * }
     */
    array() {
      return [this.x || 0, this.y || 0, this.z || 0];
    }

    /**
     * Checks whether all the vector's components are equal to another vector's.
     *
     * `equals()` returns `true` if the vector's components are all the same as another
     * vector's and `false` if not.
     *
     * The version of `equals()` with one parameter interprets it as another
     * <a href="#/p5.Vector">p5.Vector</a> object.
     *
     * The version of `equals()` with multiple parameters interprets them as the
     * components of another vector. Any missing parameters are assigned the value
     * 0.
     *
     * The static version of `equals()`, as in `p5.Vector.equals(v0, v1)`,
     * interprets both parameters as <a href="#/p5.Vector">p5.Vector</a> objects.
     *
     * @param {Number} [x] x component of the vector.
     * @param {Number} [y] y component of the vector.
     * @param {Number} [z] z component of the vector.
     * @return {Boolean} whether the vectors are equal.
     * @example
     * // META:norender
     * function setup() {
     *   // Create p5.Vector objects.
     *   let v0 = createVector(10, 20, 30);
     *   let v1 = createVector(10, 20, 30);
     *   let v2 = createVector(0, 0, 0);
     *
     *   // Prints "true" to the console.
     *   print(v0.equals(v1));
     *
     *   // Prints "false" to the console.
     *   print(v0.equals(v2));
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create p5.Vector objects.
     *   let v0 = createVector(5, 10, 20);
     *   let v1 = createVector(5, 10, 20);
     *   let v2 = createVector(13, 10, 19);
     *
     *   // Prints "true" to the console.
     *   print(v0.equals(v1.x, v1.y, v1.z));
     *
     *   // Prints "false" to the console.
     *   print(v0.equals(v2.x, v2.y, v2.z));
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create p5.Vector objects.
     *   let v0 = createVector(10, 20, 30);
     *   let v1 = createVector(10, 20, 30);
     *   let v2 = createVector(0, 0, 0);
     *
     *   // Prints "true" to the console.
     *   print(p5.Vector.equals(v0, v1));
     *
     *   // Prints "false" to the console.
     *   print(p5.Vector.equals(v0, v2));
     * }
     */
    /**
     * @param {p5.Vector|Array} value vector to compare.
     * @return {Boolean}
     */
    equals(...args) {
      let values;
      if (args[0] instanceof Vector) {
        values = args[0]._values;
      } else if (Array.isArray(args[0])) {
        values = args[0];
      } else {
        values = args;
      }

      for (let i = 0; i < this._values.length; i++) {
        if (this._values[i] !== (values[i] || 0)) {
          return false;
        }
      }
      return true;
    }

    /**
     * Replaces the components of a <a href="#/p5.Vector">p5.Vector</a> that are very close to zero with zero.
     *
     * In computers, handling numbers with decimals can give slightly imprecise answers due to the way those numbers are represented.
     * This can make it hard to check if a number is zero, as it may be close but not exactly zero.
     * This method rounds very close numbers to zero to make those checks easier
     *
     * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON
     *
     * @return {p5.Vector} with components very close to zero replaced with zero.
     * @chainable
     */
    clampToZero() {
      for (let i = 0; i < this._values.length; i++) {
        this._values[i] = this._clampToZero(this._values[i]);
      }
      return this;
    }

    /**
     * Helper function for clampToZero
     * @private
     */
    _clampToZero(val) {
      return Math.abs((val || 0) - 0) <= Number.EPSILON ? 0 : val;
    }

    // Static Methods

    /**
     * Creates a new 2D vector from an angle.
     *
     * @static
     * @param {Number}     angle desired angle, in radians. Unaffected by <a href="#/p5/angleMode">angleMode()</a>.
     * @param {Number}     [length] length of the new vector (defaults to 1).
     * @return {p5.Vector}       new <a href="#/p5.Vector">p5.Vector</a> object.
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v = p5.Vector.fromAngle(0);
     *
     *   // Prints "p5.Vector Object : [1, 0, 0]" to the console.
     *   print(v.toString());
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v = p5.Vector.fromAngle(0, 30);
     *
     *   // Prints "p5.Vector Object : [30, 0, 0]" to the console.
     *   print(v.toString());
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A black arrow extends from the center of a gray square. It points to the right.');
     * }
     * function draw() {
     *   background(200);
     *
     *   // Create a p5.Vector to the center.
     *   let v0 = createVector(50, 50);
     *
     *   // Create a p5.Vector with an angle 0 and magnitude 30.
     *   let v1 = p5.Vector.fromAngle(0, 30);
     *
     *   // Draw the black arrow.
     *   drawArrow(v0, v1, 'black');
     * }
     *
     * // Draws an arrow between two vectors.
     * function drawArrow(base, vec, myColor) {
     *   push();
     *   stroke(myColor);
     *   strokeWeight(3);
     *   fill(myColor);
     *   translate(base.x, base.y);
     *   line(0, 0, vec.x, vec.y);
     *   rotate(vec.heading());
     *   let arrowSize = 7;
     *   translate(vec.mag() - arrowSize, 0);
     *   triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
     *   pop();
     * }
     */
    static fromAngle(angle, length) {
      if (typeof length === 'undefined') {
        length = 1;
      }
      return new Vector(length * Math.cos(angle), length * Math.sin(angle));
    }

    /**
     * Creates a new 3D vector from a pair of ISO spherical angles.
     *
     * @static
     * @param {Number}     theta    polar angle in radians (zero is up).
     * @param {Number}     phi      azimuthal angle in radians
     *                               (zero is out of the screen).
     * @param {Number}     [length] length of the new vector (defaults to 1).
     * @return {p5.Vector}          new <a href="#/p5.Vector">p5.Vector</a> object.
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v = p5.Vector.fromAngles(0, 0);
     *
     *   // Prints "p5.Vector Object : [0, -1, 0]" to the console.
     *   print(v.toString());
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A light shines on a pink sphere as it orbits.');
     * }
     *
     * function draw() {
     *   background(0);
     *
     *   // Calculate the ISO angles.
     *   let theta = frameCount *  0.05;
     *   let phi = 0;
     *
     *   // Create a p5.Vector object.
     *   let v = p5.Vector.fromAngles(theta, phi, 100);
     *
     *   // Create a point light using the p5.Vector.
     *   let c = color('deeppink');
     *   pointLight(c, v);
     *
     *   // Style the sphere.
     *   fill(255);
     *   noStroke();
     *
     *   // Draw the sphere.
     *   sphere(35);
     * }
     */
    static fromAngles(theta, phi, length) {
      if (typeof length === 'undefined') {
        length = 1;
      }
      const cosPhi = Math.cos(phi);
      const sinPhi = Math.sin(phi);
      const cosTheta = Math.cos(theta);
      const sinTheta = Math.sin(theta);

      return new Vector(
        length * sinTheta * sinPhi,
        -length * cosTheta,
        length * sinTheta * cosPhi
      );
    }

    /**
     * Creates a new 2D unit vector with a random heading.
     *
     * @static
     * @return {p5.Vector} new <a href="#/p5.Vector">p5.Vector</a> object.
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v = p5.Vector.random2D();
     *
     *   // Prints "p5.Vector Object : [x, y]" to the console
     *   // where x and y are small random numbers.
     *   print(v.toString());
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Slow the frame rate.
     *   frameRate(1);
     *
     *   describe('A black arrow in extends from the center of a gray square. It changes direction once per second.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Create a p5.Vector to the center.
     *   let v0 = createVector(50, 50);
     *
     *   // Create a random p5.Vector.
     *   let v1 = p5.Vector.random2D();
     *
     *   // Scale v1 for drawing.
     *   v1.mult(30);
     *
     *   // Draw the black arrow.
     *   drawArrow(v0, v1, 'black');
     * }
     *
     * // Draws an arrow between two vectors.
     * function drawArrow(base, vec, myColor) {
     *   push();
     *   stroke(myColor);
     *   strokeWeight(3);
     *   fill(myColor);
     *   translate(base.x, base.y);
     *   line(0, 0, vec.x, vec.y);
     *   rotate(vec.heading());
     *   let arrowSize = 7;
     *   translate(vec.mag() - arrowSize, 0);
     *   triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0);
     *   pop();
     * }
     */
    static random2D() {
      return this.fromAngle(Math.random() * TWO_PI);
    }

    /**
     * Creates a new 3D unit vector with a random heading.
     *
     * @static
     * @return {p5.Vector} new <a href="#/p5.Vector">p5.Vector</a> object.
     * @example
     * // META:norender
     * function setup() {
     *   // Create a p5.Vector object.
     *   let v = p5.Vector.random3D();
     *
     *   // Prints "p5.Vector Object : [x, y, z]" to the console
     *   // where x, y, and z are small random numbers.
     *   print(v.toString());
     * }
     */
    static random3D() {
      const angle = Math.random() * TWO_PI;
      const vz = Math.random() * 2 - 1;
      const vzBase = Math.sqrt(1 - vz * vz);
      const vx = vzBase * Math.cos(angle);
      const vy = vzBase * Math.sin(angle);
      return new Vector(vx, vy, vz);
    }

    // Returns a copy of a vector.
    /**
     * @static
     * @param  {p5.Vector} v the <a href="#/p5.Vector">p5.Vector</a> to create a copy of
     * @return {p5.Vector} the copy of the <a href="#/p5.Vector">p5.Vector</a> object
     */
    static copy(v) {
      return v.copy(v);
    }

    // Adds two vectors together and returns a new one.
    /**
     * @static
     * @param  {p5.Vector} v1 A <a href="#/p5.Vector">p5.Vector</a> to add
     * @param  {p5.Vector} v2 A <a href="#/p5.Vector">p5.Vector</a> to add
     * @param  {p5.Vector} [target] vector to receive the result.
     * @return {p5.Vector} resulting <a href="#/p5.Vector">p5.Vector</a>.
     */
    static add(v1, v2, target) {
      if (!target) {
        target = v1.copy();
        if (arguments.length === 3) {
          p5._friendlyError(
            'The target parameter is undefined, it should be of type p5.Vector',
            'p5.Vector.add'
          );
        }
      } else {
        target.set(v1);
      }
      target.add(v2);
      return target;
    }

    // Returns a vector remainder when it is divided by another vector
    /**
     * @static
     * @param  {p5.Vector} v1 The dividend <a href="#/p5.Vector">p5.Vector</a>
     * @param  {p5.Vector} v2 The divisor <a href="#/p5.Vector">p5.Vector</a>
     */
    /**
     * @static
     * @param  {p5.Vector} v1
     * @param  {p5.Vector} v2
     * @return {p5.Vector} The resulting <a href="#/p5.Vector">p5.Vector</a>
     */
    static rem(v1, v2) {
      if (v1 instanceof Vector && v2 instanceof Vector) {
        let target = v1.copy();
        target.rem(v2);
        return target;
      }
    }

    /*
     * Subtracts one <a href="#/p5.Vector">p5.Vector</a> from another and returns a new one.  The second
     * vector (`v2`) is subtracted from the first (`v1`), resulting in `v1-v2`.
     */
    /**
     * @static
     * @param  {p5.Vector} v1 A <a href="#/p5.Vector">p5.Vector</a> to subtract from
     * @param  {p5.Vector} v2 A <a href="#/p5.Vector">p5.Vector</a> to subtract
     * @param  {p5.Vector} [target] vector to receive the result.
     * @return {p5.Vector} The resulting <a href="#/p5.Vector">p5.Vector</a>
     */
    static sub(v1, v2, target) {
      if (!target) {
        target = v1.copy();
        if (arguments.length === 3) {
          p5._friendlyError(
            'The target parameter is undefined, it should be of type p5.Vector',
            'p5.Vector.sub'
          );
        }
      } else {
        target.set(v1);
      }
      target.sub(v2);
      return target;
    }

    /**
     * Multiplies a vector by a scalar and returns a new vector.
     */
    /**
     * @static
     * @param  {Number} x
     * @param  {Number} y
     * @param  {Number} [z]
     * @return {p5.Vector} resulting new <a href="#/p5.Vector">p5.Vector</a>.
     */
    /**
     * @static
     * @param  {p5.Vector} v
     * @param  {Number}  n
     * @param  {p5.Vector} [target] vector to receive the result.
     */
    /**
     * @static
     * @param  {p5.Vector} v0
     * @param  {p5.Vector} v1
     * @param  {p5.Vector} [target]
     */
    /**
     * @static
     * @param  {p5.Vector} v0
     * @param  {Number[]} arr
     * @param  {p5.Vector} [target]
     */
    static mult(v, n, target) {
      if (!target) {
        target = v.copy();
        if (arguments.length === 3) {
          p5._friendlyError(
            'The target parameter is undefined, it should be of type p5.Vector',
            'p5.Vector.mult'
          );
        }
      } else {
        target.set(v);
      }
      target.mult(n);
      return target;
    }

    /**
     * Rotates the vector (only 2D vectors) by the given angle; magnitude remains the same. Returns a new vector.
     */
    /**
     * @static
     * @param  {p5.Vector} v
     * @param  {Number} angle
     * @param  {p5.Vector} [target] The vector to receive the result
     */
    static rotate(v, a, target) {
      if (arguments.length === 2) {
        target = v.copy();
      } else {
        if (!(target instanceof Vector)) {
          p5._friendlyError(
            'The target parameter should be of type p5.Vector',
            'p5.Vector.rotate'
          );
        }
        target.set(v);
      }
      target.rotate(a);
      return target;
    }

    /**
     * Divides a vector by a scalar and returns a new vector.
     */
    /**
     * @static
     * @param  {Number} x
     * @param  {Number} y
     * @param  {Number} [z]
     * @return {p5.Vector} The resulting new <a href="#/p5.Vector">p5.Vector</a>
     */
    /**
     * @static
     * @param  {p5.Vector} v
     * @param  {Number}  n
     * @param  {p5.Vector} [target] The vector to receive the result
     */
    /**
     * @static
     * @param  {p5.Vector} v0
     * @param  {p5.Vector} v1
     * @param  {p5.Vector} [target]
     */
    /**
     * @static
     * @param  {p5.Vector} v0
     * @param  {Number[]} arr
     * @param  {p5.Vector} [target]
     */
    static div(v, n, target) {
      if (!target) {
        target = v.copy();

        if (arguments.length === 3) {
          p5._friendlyError(
            'The target parameter is undefined, it should be of type p5.Vector',
            'p5.Vector.div'
          );
        }
      } else {
        target.set(v);
      }
      target.div(n);
      return target;
    }

    /**
     * Calculates the dot product of two vectors.
     */
    /**
     * @static
     * @param  {p5.Vector} v1 first <a href="#/p5.Vector">p5.Vector</a>.
     * @param  {p5.Vector} v2 second <a href="#/p5.Vector">p5.Vector</a>.
     * @return {Number}     dot product.
     */
    static dot(v1, v2) {
      return v1.dot(v2);
    }

    /**
     * Calculates the cross product of two vectors.
     */
    /**
     * @static
     * @param  {p5.Vector} v1 first <a href="#/p5.Vector">p5.Vector</a>.
     * @param  {p5.Vector} v2 second <a href="#/p5.Vector">p5.Vector</a>.
     * @return {p5.Vector}     cross product.
     */
    static cross(v1, v2) {
      return v1.cross(v2);
    }

    /**
     * Calculates the Euclidean distance between two points (considering a
     * point as a vector object).
     */
    /**
     * @static
     * @param  {p5.Vector} v1 The first <a href="#/p5.Vector">p5.Vector</a>
     * @param  {p5.Vector} v2 The second <a href="#/p5.Vector">p5.Vector</a>
     * @return {Number}     The distance
     */
    static dist(v1, v2) {
      return v1.dist(v2);
    }

    /**
     * Linear interpolate a vector to another vector and return the result as a
     * new vector.
     */
    /**
     * @static
     * @param {p5.Vector} v1
     * @param {p5.Vector} v2
     * @param {Number} amt
     * @param {p5.Vector} [target] The vector to receive the result
     * @return {p5.Vector}      The lerped value
     */
    static lerp(v1, v2, amt, target) {
      if (!target) {
        target = v1.copy();
        if (arguments.length === 4) {
          p5._friendlyError(
            'The target parameter is undefined, it should be of type p5.Vector',
            'p5.Vector.lerp'
          );
        }
      } else {
        target.set(v1);
      }
      target.lerp(v2, amt);
      return target;
    }

    /**
     * Performs spherical linear interpolation with the other vector
     * and returns the resulting vector.
     * This works in both 3D and 2D. As for 2D, the result of slerping
     * between 2D vectors is always a 2D vector.
     */
    /**
     * @static
     * @param {p5.Vector} v1 old vector.
     * @param {p5.Vector} v2 new vector.
     * @param {Number} amt
     * @param {p5.Vector} [target] vector to receive the result.
     * @return {p5.Vector} slerped vector between v1 and v2
     */
    static slerp(v1, v2, amt, target) {
      if (!target) {
        target = v1.copy();
        if (arguments.length === 4) {
          p5._friendlyError(
            'The target parameter is undefined, it should be of type p5.Vector',
            'p5.Vector.slerp'
          );
        }
      } else {
        target.set(v1);
      }
      target.slerp(v2, amt);
      return target;
    }

    /**
     * Calculates the magnitude (length) of the vector and returns the result as
     * a float (this is simply the equation `sqrt(x*x + y*y + z*z)`.)
     */
    /**
     * @static
     * @param {p5.Vector} vecT The vector to return the magnitude of
     * @return {Number}        The magnitude of vecT
     */
    static mag(vecT) {
      return vecT.mag();
    }

    /**
     * Calculates the squared magnitude of the vector and returns the result
     * as a float (this is simply the equation <em>(x\*x + y\*y + z\*z)</em>.)
     * Faster if the real length is not required in the
     * case of comparing vectors, etc.
     */
    /**
     * @static
     * @param {p5.Vector} vecT the vector to return the squared magnitude of
     * @return {Number}        the squared magnitude of vecT
     */
    static magSq(vecT) {
      return vecT.magSq();
    }

    /**
     * Normalize the vector to length 1 (make it a unit vector).
     */
    /**
     * @static
     * @param {p5.Vector} v  The vector to normalize
     * @param {p5.Vector} [target] The vector to receive the result
     * @return {p5.Vector}   The vector v, normalized to a length of 1
     */
    static normalize(v, target) {
      if (arguments.length < 2) {
        target = v.copy();
      } else {
        if (!(target instanceof Vector)) {
          p5._friendlyError(
            'The target parameter should be of type p5.Vector',
            'p5.Vector.normalize'
          );
        }
        target.set(v);
      }
      return target.normalize();
    }

    /**
     * Limit the magnitude of the vector to the value used for the <b>max</b>
     * parameter.
     */
    /**
     * @static
     * @param {p5.Vector} v  the vector to limit
     * @param {Number}    max
     * @param {p5.Vector} [target] the vector to receive the result (Optional)
     * @return {p5.Vector} v with a magnitude limited to max
     */
    static limit(v, max, target) {
      if (arguments.length < 3) {
        target = v.copy();
      } else {
        if (!(target instanceof Vector)) {
          p5._friendlyError(
            'The target parameter should be of type p5.Vector',
            'p5.Vector.limit'
          );
        }
        target.set(v);
      }
      return target.limit(max);
    }

    /**
     * Set the magnitude of the vector to the value used for the <b>len</b>
     * parameter.
     */
    /**
     * @static
     * @param {p5.Vector} v  the vector to set the magnitude of
     * @param {Number}    len
     * @param {p5.Vector} [target] the vector to receive the result (Optional)
     * @return {p5.Vector} v with a magnitude set to len
     */
    static setMag(v, len, target) {
      if (arguments.length < 3) {
        target = v.copy();
      } else {
        if (!(target instanceof Vector)) {
          p5._friendlyError(
            'The target parameter should be of type p5.Vector',
            'p5.Vector.setMag'
          );
        }
        target.set(v);
      }
      return target.setMag(len);
    }

    /**
     * Calculate the angle of rotation for this vector (only 2D vectors).
     * p5.Vectors created using <a href="#/p5/createVector">createVector()</a>
     * will take the current <a href="#/p5/angleMode">angleMode</a> into
     * consideration, and give the angle in radians or degrees accordingly.
     */
    /**
     * @static
     * @param {p5.Vector} v the vector to find the angle of
     * @return {Number} the angle of rotation
     */
    static heading(v) {
      return v.heading();
    }

    /**
     * Calculates and returns the angle between two vectors. This function will take
     * the <a href="#/p5/angleMode">angleMode</a> on v1 into consideration, and
     * give the angle in radians or degrees accordingly.
     */
    /**
     * @static
     * @param  {p5.Vector}    v1 the first vector.
     * @param  {p5.Vector}    v2 the second vector.
     * @return {Number}       angle between the two vectors.
     */
    static angleBetween(v1, v2) {
      return v1.angleBetween(v2);
    }

    /**
     * Reflect a vector about a normal to a line in 2D, or about a normal to a
     * plane in 3D.
     */
    /**
     * @static
     * @param  {p5.Vector} incidentVector vector to be reflected.
     * @param  {p5.Vector} surfaceNormal
     * @param  {p5.Vector} [target] vector to receive the result.
     * @return {p5.Vector} the reflected vector
     */
    static reflect(incidentVector, surfaceNormal, target) {
      if (arguments.length < 3) {
        target = incidentVector.copy();
      } else {
        if (!(target instanceof Vector)) {
          p5._friendlyError(
            'The target parameter should be of type p5.Vector',
            'p5.Vector.reflect'
          );
        }
        target.set(incidentVector);
      }
      return target.reflect(surfaceNormal);
    }

    /**
     * Return a representation of this vector as a float array. This is only
     * for temporary use. If used in any other fashion, the contents should be
     * copied by using the <b>p5.Vector.<a href="#/p5.Vector/copy">copy()</a></b>
     * method to copy into your own vector.
     */
    /**
     * @static
     * @param  {p5.Vector} v the vector to convert to an array
     * @return {Number[]} an Array with the 3 values
     */
    static array(v) {
      return v.array();
    }

    /**
     * Equality check against a <a href="#/p5.Vector">p5.Vector</a>
     */
    /**
     * @static
     * @param {p5.Vector|Array} v1 the first vector to compare
     * @param {p5.Vector|Array} v2 the second vector to compare
     * @return {Boolean}
     */
    static equals(v1, v2) {
      let v;
      if (v1 instanceof Vector) {
        v = v1;
      } else if (v1 instanceof Array) {
        v = new Vector().set(v1);
      } else {
        p5._friendlyError(
          'The v1 parameter should be of type Array or p5.Vector',
          'p5.Vector.equals'
        );
      }
      return v.equals(v2);
    }
  }

  function vector(p5, fn) {
    /**
     * A class to describe a two or three-dimensional vector.
     *
     * A vector can be thought of in different ways. In one view, a vector is like
     * an arrow pointing in space. Vectors have both magnitude (length) and
     * direction.
     *
     * `p5.Vector` objects are often used to program motion because they simplify
     * the math. For example, a moving ball has a position and a velocity.
     * Position describes where the ball is in space. The ball's position vector
     * extends from the origin to the ball's center. Velocity describes the ball's
     * speed and the direction it's moving. If the ball is moving straight up, its
     * velocity vector points straight up. Adding the ball's velocity vector to
     * its position vector moves it, as in `pos.add(vel)`. Vector math relies on
     * methods inside the `p5.Vector` class.
     *
     * Note: <a href="#/p5/createVector">createVector()</a> is the recommended way
     * to make an instance of this class.
     *
     * @class p5.Vector
     * @param {Number} [x] x component of the vector.
     * @param {Number} [y] y component of the vector.
     * @param {Number} [z] z component of the vector.
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create p5.Vector objects.
     *   let p1 = createVector(25, 25);
     *   let p2 = createVector(75, 75);
     *
     *   // Style the points.
     *   strokeWeight(5);
     *
     *   // Draw the first point using a p5.Vector.
     *   point(p1);
     *
     *   // Draw the second point using a p5.Vector's components.
     *   point(p2.x, p2.y);
     *
     *   describe('Two black dots on a gray square, one at the top left and the other at the bottom right.');
     * }
     *
     * @example
     * let pos;
     * let vel;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create p5.Vector objects.
     *   pos = createVector(50, 100);
     *   vel = createVector(0, -1);
     *
     *   describe('A black dot moves from bottom to top on a gray square. The dot reappears at the bottom when it reaches the top.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Add velocity to position.
     *   pos.add(vel);
     *
     *   // If the dot reaches the top of the canvas,
     *   // restart from the bottom.
     *   if (pos.y < 0) {
     *     pos.y = 100;
     *   }
     *
     *   // Draw the dot.
     *   strokeWeight(5);
     *   point(pos);
     * }
     */
    p5.Vector = Vector;

    /**
     * The x component of the vector
     * @type {Number}
     * @for p5.Vector
     * @property x
     * @name x
     */

    /**
     * The y component of the vector
     * @type {Number}
     * @for p5.Vector
     * @property y
     * @name y
     */

    /**
     * The z component of the vector
     * @type {Number}
     * @for p5.Vector
     * @property z
     * @name z
     */
  }

  if (typeof p5 !== 'undefined') {
    vector(p5);
  }

  /**
   * @module Shape
   * @submodule Custom Shapes
   * @for p5
   * @requires core
   * @requires constants
   */


  // ---- UTILITY FUNCTIONS ----
  function polylineLength(vertices) {
    let length = 0;
    for (let i = 1; i < vertices.length; i++) {
      length += vertices[i-1].position.dist(vertices[i].position);
    }
    return length;
  }

  // ---- GENERAL BUILDING BLOCKS ----

  class Vertex {
    constructor(properties) {
      for (const [key, value] of Object.entries(properties)) {
        this[key] = value;
      }
    }
    /*
    get array() {
      // convert to 1D array
      // call `toArray()` if value is an object with a toArray() method
      // handle primitive values separately
      // maybe handle object literals too, with Object.values()?
      // probably don’t need anything else for now?
    }
    */
    // TODO: make sure name of array conversion method is
    // consistent with any modifications to the names of corresponding
    // properties of p5.Vector and p5.Color
  }

  class ShapePrimitive {
    vertices;
    _shape = null;
    _primitivesIndex = null;
    _contoursIndex = null;
    isClosing = false;

    constructor(...vertices) {
      if (this.constructor === ShapePrimitive) {
        throw new Error('ShapePrimitive is an abstract class: it cannot be instantiated.');
      }
      if (vertices.length > 0) {
        this.vertices = vertices;
      }
      else {
        throw new Error('At least one vertex must be passed to the constructor.');
      }
    }

    get vertexCount() {
      return this.vertices.length;
    }

    get vertexCapacity() {
      throw new Error('Getter vertexCapacity must be implemented.');
    }

    get _firstInterpolatedVertex() {
      return this.startVertex();
    }

    get canOverrideAnchor() {
      return false;
    }

    accept(visitor) {
      throw new Error('Method accept() must be implemented.');
    }

    addToShape(shape) {
      /*
      TODO:
      Refactor?
      Test this method once more primitives are implemented.
      Test segments separately (Segment adds an extra step to this method).
      */
      let lastContour = shape.at(-1);

      if (lastContour.primitives.length === 0) {
        lastContour.primitives.push(this);
      } else {
        // last primitive in shape
        let lastPrimitive = shape.at(-1, -1);
        let hasSameType = lastPrimitive instanceof this.constructor;
        let spareCapacity = lastPrimitive.vertexCapacity -
                            lastPrimitive.vertexCount;

        // this primitive
        let pushableVertices;
        let remainingVertices;

        if (hasSameType && spareCapacity > 0) {

          pushableVertices = this.vertices.splice(0, spareCapacity);
          remainingVertices = this.vertices;
          lastPrimitive.vertices.push(...pushableVertices);

          if (remainingVertices.length > 0) {
            lastContour.primitives.push(this);
          }
        }
        else {
          lastContour.primitives.push(this);
        }
      }

      // if primitive itself was added
      // (i.e. its individual vertices weren't all added to an existing primitive)
      // give it a reference to the shape and store its location within the shape
      let addedToShape = this.vertices.length > 0;
      if (addedToShape) {
        let lastContour = shape.at(-1);
        this._primitivesIndex = lastContour.primitives.length - 1;
        this._contoursIndex = shape.contours.length - 1;
        this._shape = shape;
      }

      return shape.at(-1, -1);
    }

    get _nextPrimitive() {
      return this._belongsToShape ?
        this._shape.at(this._contoursIndex, this._primitivesIndex + 1) :
        null;
    }

    get _belongsToShape() {
      return this._shape !== null;
    }

    handlesClose() {
      return false;
    }

    close(vertex) {
      throw new Error('Unimplemented!');
    }
  }

  class Contour {
    #kind;
    primitives;

    constructor(kind = PATH) {
      this.#kind = kind;
      this.primitives = [];
    }

    get kind() {
      const isEmpty = this.primitives.length === 0;
      const isPath = this.#kind === PATH;
      return isEmpty && isPath ? EMPTY_PATH : this.#kind;
    }

    accept(visitor) {
      for (const primitive of this.primitives) {
        primitive.accept(visitor);
      }
    }
  }

  // ---- PATH PRIMITIVES ----

  class Anchor extends ShapePrimitive {
    #vertexCapacity = 1;

    get vertexCapacity() {
      return this.#vertexCapacity;
    }

    accept(visitor) {
      visitor.visitAnchor(this);
    }

    getEndVertex() {
      return this.vertices[0];
    }
  }

  // abstract class
  class Segment extends ShapePrimitive {
    constructor(...vertices) {
      super(...vertices);
      if (this.constructor === Segment) {
        throw new Error('Segment is an abstract class: it cannot be instantiated.');
      }
    }

    // segments in a shape always have a predecessor
    // (either an anchor or another segment)
    get _previousPrimitive() {
      return this._belongsToShape ?
        this._shape.at(this._contoursIndex, this._primitivesIndex - 1) :
        null;
    }

    getStartVertex() {
      return this._previousPrimitive.getEndVertex();
    }

    getEndVertex() {
      return this.vertices.at(-1);
    }
  }

  class LineSegment extends Segment {
    #vertexCapacity = 1;

    get vertexCapacity() {
      return this.#vertexCapacity;
    }

    accept(visitor) {
      visitor.visitLineSegment(this);
    }
  }

  let BezierSegment$1 = class BezierSegment extends Segment {
    #order;
    #vertexCapacity;

    constructor(order, ...vertices) {
      super(...vertices);

      // Order m may sometimes be passed as an array [m], since arrays
      // may be used elsewhere to store order of
      // Bezier curves and surfaces in a common format

      let numericalOrder = Array.isArray(order) ? order[0] : order;
      this.#order = numericalOrder;
      this.#vertexCapacity = numericalOrder;
    }

    get order() {
      return this.#order;
    }

    get vertexCapacity() {
      return this.#vertexCapacity;
    }

    #_hullLength;
    hullLength() {
      if (this.#_hullLength === undefined) {
        this.#_hullLength = polylineLength([
          this.getStartVertex(),
          ...this.vertices
        ]);
      }
      return this.#_hullLength;
    }

    accept(visitor) {
      visitor.visitBezierSegment(this);
    }
  };

  /*
  To-do: Consider type and end modes -- see #6766
  may want to use separate classes, but maybe not

  For now, the implementation overrides
  super.getEndVertex() in order to preserve current p5
  endpoint behavior, but we're considering defaulting
  to interpolated endpoints (a breaking change)
  */
  class SplineSegment extends Segment {
    #vertexCapacity = Infinity;
    _splineProperties = {
      ends: INCLUDE,
      tightness: 0
    };

    get vertexCapacity() {
      return this.#vertexCapacity;
    }

    accept(visitor) {
      visitor.visitSplineSegment(this);
    }

    get _comesAfterSegment() {
      return this._previousPrimitive instanceof Segment;
    }

    get canOverrideAnchor() {
      return this._splineProperties.ends === EXCLUDE;
    }

    // assuming for now that the first interpolated vertex is always
    // the second vertex passed to splineVertex()
    // if this spline segment doesn't follow another segment,
    // the first vertex is in an anchor
    get _firstInterpolatedVertex() {
      if (this._splineProperties.ends === EXCLUDE) {
        return this._comesAfterSegment ?
          this.vertices[1] :
          this.vertices[0];
      } else {
        return this.getStartVertex();
      }
    }

    get _chainedToSegment() {
      if (this._belongsToShape && this._comesAfterSegment) {
        let interpolatedStartPosition = this._firstInterpolatedVertex.position;
        let predecessorEndPosition = this.getStartVertex().position;
        return predecessorEndPosition.equals(interpolatedStartPosition);
      }
      else {
        return false;
      }
    }

    // extend addToShape() with a warning in case second vertex
    // doesn't line up with end of last segment
    addToShape(shape) {
      const added = super.addToShape(shape);
      this._splineProperties.ends = shape._splineProperties.ends;
      this._splineProperties.tightness = shape._splineProperties.tightness;

      if (this._splineProperties.ends !== EXCLUDE) return added;

      let verticesPushed = !this._belongsToShape;
      let lastPrimitive = shape.at(-1, -1);

      let message = (array1, array2) =>
        `Spline does not start where previous path segment ends:
      second spline vertex at (${array1})
      expected to be at (${array2}).`;

      if (verticesPushed &&
        // Only check once the first interpolated vertex has been added
        lastPrimitive.vertices.length === 2 &&
        lastPrimitive._comesAfterSegment &&
        !lastPrimitive._chainedToSegment
      ) {
        let interpolatedStart = lastPrimitive._firstInterpolatedVertex.position;
        let predecessorEnd = lastPrimitive.getStartVertex().position;

        console.warn(
          message(interpolatedStart.array(), predecessorEnd.array())
        );
      }

      // Note: Could add a warning in an else-if case for when this spline segment
      // is added directly to the shape instead of pushing its vertices to
      // an existing spline segment. However, if we assume addToShape() is called by
      // splineVertex(), it'd add a new spline segment with only one vertex in that case,
      // and the check wouldn't be needed yet.

      // TODO: Consider case where positions match but other vertex properties don't.
      return added;
    }

    // override method on base class
    getEndVertex() {
      if (this._splineProperties.ends === INCLUDE) {
        return super.getEndVertex();
      } else if (this._splineProperties.ends === EXCLUDE) {
        return this.vertices.at(-2);
      } else {
        return this.getStartVertex();
      }
    }

    getControlPoints() {
      let points = [];

      if (this._comesAfterSegment) {
        points.push(this.getStartVertex());
      }
      points.push(this.getStartVertex());

      for (const vertex of this.vertices) {
        points.push(vertex);
      }

      const prevVertex = this.getStartVertex();
      if (this._splineProperties.ends === INCLUDE) {
        points.unshift(prevVertex);
        points.push(this.vertices.at(-1));
      } else if (this._splineProperties.ends === JOIN) {
        points.unshift(this.vertices.at(-1));
        points.push(prevVertex, this.vertices.at(0));
      }

      return points;
    }

    handlesClose() {
      if (!this._belongsToShape) return false;

      // Only handle closing if the spline is the only thing in its contour after
      // the anchor
      const contour = this._shape.at(this._contoursIndex);
      return contour.primitives.length === 2 && this._primitivesIndex === 1;
    }

    close() {
      this._splineProperties.ends = JOIN;
    }
  }

  // ---- ISOLATED PRIMITIVES ----

  class Point extends ShapePrimitive {
    #vertexCapacity = 1;

    get vertexCapacity() {
      return this.#vertexCapacity;
    }

    accept(visitor) {
      visitor.visitPoint(this);
    }
  }

  class Line extends ShapePrimitive {
    #vertexCapacity = 2;

    get vertexCapacity() {
      return this.#vertexCapacity;
    }

    accept(visitor) {
      visitor.visitLine(this);
    }
  }

  class Triangle extends ShapePrimitive {
    #vertexCapacity = 3;

    get vertexCapacity() {
      return this.#vertexCapacity;
    }

    accept(visitor) {
      visitor.visitTriangle(this);
    }
  }

  class Quad extends ShapePrimitive {
    #vertexCapacity = 4;

    get vertexCapacity() {
      return this.#vertexCapacity;
    }

    accept(visitor) {
      visitor.visitQuad(this);
    }
  }

  // ---- TESSELLATION PRIMITIVES ----

  class TriangleFan extends ShapePrimitive {
    #vertexCapacity = Infinity;

    get vertexCapacity() {
      return this.#vertexCapacity;
    }

    accept(visitor) {
      visitor.visitTriangleFan(this);
    }
  }

  class TriangleStrip extends ShapePrimitive {
    #vertexCapacity = Infinity;

    get vertexCapacity() {
      return this.#vertexCapacity;
    }

    accept(visitor) {
      visitor.visitTriangleStrip(this);
    }
  }

  class QuadStrip extends ShapePrimitive {
    #vertexCapacity = Infinity;

    get vertexCapacity() {
      return this.#vertexCapacity;
    }

    accept(visitor) {
      visitor.visitQuadStrip(this);
    }
  }

  // ---- PRIMITIVE SHAPE CREATORS ----

  class PrimitiveShapeCreators {
    // TODO: make creators private?
    // That'd probably be better, but for now, it may be convenient to use
    // native Map properties like size, e.g. for testing, and it's simpler to
    // not have to wrap all the properties that might be useful
    creators;

    constructor() {
      let creators = new Map();

      /* TODO: REFACTOR BASED ON THE CODE BELOW,
         ONCE CONSTANTS ARE IMPLEMENTED AS SYMBOLS

      // Store Symbols as strings for use in Map keys
      const EMPTY_PATH = constants.EMPTY_PATH.description;
      const PATH = constants.PATH.description;
      //etc.

      creators.set(`vertex-${EMPTY_PATH}`, (...vertices) => new Anchor(...vertices));
      // etc.

      get(vertexKind, shapeKind) {
        const key = `${vertexKind}-${shapeKind.description}`;
        return this.creators.get(key);
      }
      // etc.
      */

      // vertex
      creators.set(`vertex-${EMPTY_PATH}`, (...vertices) => new Anchor(...vertices));
      creators.set(`vertex-${PATH}`, (...vertices) => new LineSegment(...vertices));
      creators.set(`vertex-${POINTS}`, (...vertices) => new Point(...vertices));
      creators.set(`vertex-${LINES}`, (...vertices) => new Line(...vertices));
      creators.set(`vertex-${TRIANGLES}`, (...vertices) => new Triangle(...vertices));
      creators.set(`vertex-${QUADS}`, (...vertices) => new Quad(...vertices));
      creators.set(`vertex-${TRIANGLE_FAN}`, (...vertices) => new TriangleFan(...vertices));
      creators.set(`vertex-${TRIANGLE_STRIP}`, (...vertices) => new TriangleStrip(...vertices));
      creators.set(`vertex-${QUAD_STRIP}`, (...vertices) => new QuadStrip(...vertices));

      // bezierVertex (constructors all take order and vertices so they can be called in a uniform way)
      creators.set(`bezierVertex-${EMPTY_PATH}`, (order, ...vertices) => new Anchor(...vertices));
      creators.set(`bezierVertex-${PATH}`, (order, ...vertices) => new BezierSegment$1(order, ...vertices));

      // splineVertex
      creators.set(`splineVertex-${EMPTY_PATH}`, (...vertices) => new Anchor(...vertices));
      creators.set(`splineVertex-${PATH}`, (...vertices) => new SplineSegment(...vertices));

      this.creators = creators;
    }

    get(vertexKind, shapeKind) {
      const key = `${vertexKind}-${shapeKind}`;
      return this.creators.get(key);
    }

    set(vertexKind, shapeKind, creator) {
      const key = `${vertexKind}-${shapeKind}`;
      this.creators.set(key, creator);
    }

    clear() {
      this.creators.clear();
    }
  }

  // ---- SHAPE ----

  /* Note: It's assumed that Shape instances are always built through
   * their beginShape()/endShape() methods. For example, this ensures
   * that a segment is never the first primitive in a contour (paths
   * always start with an anchor), which simplifies code elsewhere.
   */
  class Shape {
    #vertexProperties;
    #initialVertexProperties;
    #primitiveShapeCreators;
    #bezierOrder = 3;
    kind = null;
    contours = [];
    _splineProperties = {
      tightness: 0,
      ends: INCLUDE
    };
    userVertexProperties = null;

    constructor(
      vertexProperties,
      primitiveShapeCreators = new PrimitiveShapeCreators()
    ) {
      this.#initialVertexProperties = vertexProperties;
      this.#vertexProperties = vertexProperties;
      this.#primitiveShapeCreators = primitiveShapeCreators;

      for (const key in this.#vertexProperties) {
        if (key !== 'position' && key !== 'textureCoordinates') {
          this[key] = function(value) {
            this.#vertexProperties[key] = value;
          };
        }
      }
    }

    serializeToArray(val) {
      if (val === null || val === undefined) {
        return [];
      } if (val instanceof Number) {
        return [val];
      } else if (val instanceof Array) {
        return val;
      } else if (val.array instanceof Function) {
        return val.array();
      } else {
        throw new Error(`Can't convert ${val} to array!`);
      }
    }

    vertexToArray(vertex) {
      const array = [];
      for (const key in this.#vertexProperties) {
        if (this.userVertexProperties && key in this.userVertexProperties)
          continue;
        const val = vertex[key];
        array.push(...this.serializeToArray(val));
      }
      for (const key in this.userVertexProperties) {
        if (key in vertex) {
          array.push(...this.serializeToArray(vertex[key]));
        } else {
          array.push(...new Array(this.userVertexProperties[key]).fill(0));
        }
      }
      return array;
    }

    hydrateValue(queue, original) {
      if (original === null) {
        return null;
      } else if (original instanceof Number) {
        return queue.shift();
      } else if (original instanceof Array) {
        const array = [];
        for (let i = 0; i < original.length; i++) {
          array.push(queue.shift());
        }
        return array;
      } else if (original instanceof Vector) {
        return new Vector(queue.shift(), queue.shift(), queue.shift());
      } else if (original instanceof Color) {
        // NOTE: Not sure what intention here is, `Color` constructor signature
        // has changed so needed to be reviewed
        const array = [
          queue.shift(),
          queue.shift(),
          queue.shift(),
          queue.shift()
        ];
        return new Color(array);
      }
    }

    arrayToVertex(array) {
      const vertex = {};
      const queue = [...array];

      for (const key in this.#vertexProperties) {
        if (this.userVertexProperties && key in this.userVertexProperties)
          continue;
        const original = this.#vertexProperties[key];
        vertex[key] = this.hydrateValue(queue, original);
      }
      for (const key in this.userVertexProperties) {
        const original = this.#vertexProperties[key];
        vertex[key] = this.hydrateValue(queue, original);
      }
      return vertex;
    }

    arrayScale(array, scale) {
      return array.map(v => v * scale);
    }

    arraySum(first, ...rest) {
      return first.map((v, i) => {
        let result = v;
        for (let j = 0; j < rest.length; j++) {
          result += rest[j][i];
        }
        return result;
      });
    }

    arrayMinus(a, b) {
      return a.map((v, i) => v - b[i]);
    }

    evaluateCubicBezier([a, b, c, d], t) {
      return this.arraySum(
        this.arrayScale(a, Math.pow(1 - t, 3)),
        this.arrayScale(b, 3 * Math.pow(1 - t, 2) * t),
        this.arrayScale(c, 3 * (1 - t) * Math.pow(t, 2)),
        this.arrayScale(d, Math.pow(t, 3))
      );
    }

    evaluateQuadraticBezier([a, b, c], t) {
      return this.arraySum(
        this.arrayScale(a, Math.pow(1 - t, 2)),
        this.arrayScale(b, 2 * (1 - t) * t),
        this.arrayScale(c, t * t)
      );
    }

    /*
    catmullRomToBezier(vertices, tightness)

    Abbreviated description:
    Converts a Catmull-Rom spline to a sequence of Bezier curveTo points.

    Parameters:
    vertices -> Array [v0, v1, v2, v3, ...] of at least four vertices
    tightness -> Number affecting shape of curve

    Returns:
    array of Bezier curveTo control points, each represented as [c1, c2, c3][]

    TODO:
    1. It seems p5 contains code for converting from Catmull-Rom to Bezier in at least two places:

    catmullRomToBezier() is based on code in the legacy endShape() function:
    https://github.com/processing/p5.js/blob/1b66f097761d3c2057c0cec4349247d6125f93ca/src/core/p5.Renderer2D.js#L859C1-L886C1

    A different conversion can be found elsewhere in p5:
    https://github.com/processing/p5.js/blob/17304ce9e9ef3f967bd828102a51b62a2d39d4f4/src/typography/p5.Font.js#L1179

    A more careful review and comparison of both implementations would be helpful. They're different. I put
    catmullRomToBezier() together quickly without checking the math/algorithm, when I made the proof of concept
    for the refactor.

    2. It may be possible to replace the code in p5.Font.js with the code here, to reduce duplication.
    */
    catmullRomToBezier(vertices, tightness) {
      let s = 1 - tightness;
      let bezArrays = [];

      for (let i = 0; i + 3 < vertices.length; i++) {
        const [a, b, c, d] = vertices.slice(i, i + 4);
        const bezB = this.arraySum(
          b,
          this.arrayScale(this.arrayMinus(c, a), s / 6)
        );
        const bezC = this.arraySum(
          c,
          this.arrayScale(this.arrayMinus(b, d), s / 6)
        );
        const bezD = c;

        bezArrays.push([bezB, bezC, bezD]);
      }
      return bezArrays;
    }

    // TODO for at() method:

    // RENAME?
    // -at() indicates it works like Array.prototype.at(), e.g. with negative indices
    // -get() may work better if we want to add a corresponding set() method
    // -a set() method could maybe check for problematic usage (e.g. inserting a Triangle into a PATH)
    // -renaming or removing would necessitate changes at call sites (it's already in use)

    // REFACTOR?

    // TEST
    at(contoursIndex, primitivesIndex, verticesIndex) {
      let contour;
      let primitive;

      contour = this.contours.at(contoursIndex);

      switch(arguments.length) {
        case 1:
          return contour;
        case 2:
          return contour.primitives.at(primitivesIndex);
        case 3:
          primitive = contour.primitives.at(primitivesIndex);
          return primitive.vertices.at(verticesIndex);
      }
    }

    // maybe call this clear() for consistency with PrimitiveShapeCreators.clear()?
    // note: p5.Geometry has a reset() method, but also clearColors()
    // looks like reset() isn't in the public reference, so maybe we can switch
    // everything to clear()? Not sure if reset/clear is used in other classes,
    // but it'd be good if geometries and shapes are consistent
    reset() {
      this.#vertexProperties = { ...this.#initialVertexProperties };
      this.kind = null;
      this.contours = [];
      this.userVertexProperties = null;
    }

    vertexProperty(name, data) {
      this.userVertexProperties = this.userVertexProperties || {};
      const key = this.vertexPropertyKey(name);

      const dataArray = Array.isArray(data) ? data : [data];

      if (!this.userVertexProperties[key]) {
        this.userVertexProperties[key] = dataArray.length;
      }
      this.#vertexProperties[key] = dataArray;
    }
    vertexPropertyName(key) {
      return key.replace(/Src$/, '');
    }
    vertexPropertyKey(name) {
      return name + 'Src';
    }

    bezierOrder(...order) {
      this.#bezierOrder = order;
    }

    splineProperty(key, value) {
      this._splineProperties[key] = value;
    }

    splineProperties(values) {
      if (values) {
        for (const key in values) {
          this.splineProperty(key, values[key]);
        }
      } else {
        return this._splineProperties;
      }
    }

    /*
    To-do: Maybe refactor #createVertex() since this has side effects that aren't advertised
    in the method name?
    */
    #createVertex(position, textureCoordinates) {
      this.#vertexProperties.position = position;

      if (textureCoordinates !== undefined) {
        this.#vertexProperties.textureCoordinates = textureCoordinates;
      }

      return new Vertex(this.#vertexProperties);
    }

    #createPrimitiveShape(vertexKind, shapeKind, ...vertices) {
      let primitiveShapeCreator = this.#primitiveShapeCreators.get(
        vertexKind, shapeKind
      );

      return  vertexKind === 'bezierVertex' ?
        primitiveShapeCreator(this.#bezierOrder, ...vertices) :
        primitiveShapeCreator(...vertices);
    }

    /*
      #generalVertex() is reused by the special vertex functions,
      including vertex(), bezierVertex(), splineVertex(), and arcVertex():

      It creates a vertex, builds a primitive including that
      vertex, and has the primitive add itself to the shape.
    */
    #generalVertex(kind, position, textureCoordinates) {
      let vertexKind = kind;
      let lastContourKind = this.at(-1).kind;
      let vertex = this.#createVertex(position, textureCoordinates);

      let primitiveShape = this.#createPrimitiveShape(
        vertexKind,
        lastContourKind,
        vertex
      );

      return primitiveShape.addToShape(this);
    }

    vertex(position, textureCoordinates, { isClosing = false } = {}) {
      const added = this.#generalVertex('vertex', position, textureCoordinates);
      added.isClosing = isClosing;
    }

    bezierVertex(position, textureCoordinates) {
      this.#generalVertex('bezierVertex', position, textureCoordinates);
    }

    splineVertex(position, textureCoordinates) {
      this.#generalVertex('splineVertex', position, textureCoordinates);
    }

    arcVertex(position, textureCoordinates) {
      this.#generalVertex('arcVertex', position, textureCoordinates);
    }

    beginContour(shapeKind = PATH) {
      if (this.at(-1)?.kind === EMPTY_PATH) {
        this.contours.pop();
      }
      this.contours.push(new Contour(shapeKind));
    }

    endContour(closeMode = OPEN, _index = this.contours.length - 1) {
      const contour = this.at(_index);
      if (closeMode === CLOSE) {
        // shape characteristics
        const isPath = contour.kind === PATH;

        // anchor characteristics
        const anchorVertex = this.at(_index, 0, 0);
        const anchorHasPosition = Object.hasOwn(anchorVertex, 'position');
        const lastSegment = this.at(_index, -1);

        // close path
        if (isPath && anchorHasPosition) {
          if (lastSegment.handlesClose()) {
            lastSegment.close(anchorVertex);
          } else {
            // Temporarily remove contours after the current one so that we add to the original
            // contour again
            const rest = this.contours.splice(
              _index + 1,
              this.contours.length - _index - 1
            );
            const prevVertexProperties = this.#vertexProperties;
            this.#vertexProperties = { ...prevVertexProperties };
            for (const key in anchorVertex) {
              if (['position', 'textureCoordinates'].includes(key)) continue;
              this.#vertexProperties[key] = anchorVertex[key];
            }
            this.vertex(
              anchorVertex.position,
              anchorVertex.textureCoordinates,
              { isClosing: true }
            );
            this.#vertexProperties = prevVertexProperties;
            this.contours.push(...rest);
          }
        }
      }
    }

    beginShape(shapeKind = PATH) {
      this.kind = shapeKind;
      // Implicitly start a contour
      this.beginContour(shapeKind);
    }
    /* TO-DO:
       Refactor?
       - Might not need anchorHasPosition.
       - Might combine conditions at top, and rely on shortcircuiting.
       Does nothing if shape is not a path or has multiple contours. Might discuss this.
    */
    endShape(closeMode = OPEN) {
      if (closeMode === CLOSE) {
        // Close the first contour, the one implicitly used for shape data
        // added without an explicit contour
        this.endContour(closeMode, 0);
      }
    }

    accept(visitor) {
      for (const contour of this.contours) {
        contour.accept(visitor);
      }
    }
  }

  // ---- PRIMITIVE VISITORS ----

  // abstract class
  class PrimitiveVisitor {
    constructor() {
      if (this.constructor === PrimitiveVisitor) {
        throw new Error('PrimitiveVisitor is an abstract class: it cannot be instantiated.');
      }
    }
    // path primitives
    visitAnchor(anchor) {
      throw new Error('Method visitAnchor() has not been implemented.');
    }
    visitLineSegment(lineSegment) {
      throw new Error('Method visitLineSegment() has not been implemented.');
    }
    visitBezierSegment(bezierSegment) {
      throw new Error('Method visitBezierSegment() has not been implemented.');
    }
    visitSplineSegment(curveSegment) {
      throw new Error('Method visitSplineSegment() has not been implemented.');
    }
    visitArcSegment(arcSegment) {
      throw new Error('Method visitArcSegment() has not been implemented.');
    }

    // isolated primitives
    visitPoint(point) {
      throw new Error('Method visitPoint() has not been implemented.');
    }
    visitLine(line) {
      throw new Error('Method visitLine() has not been implemented.');
    }
    visitTriangle(triangle) {
      throw new Error('Method visitTriangle() has not been implemented.');
    }
    visitQuad(quad) {
      throw new Error('Method visitQuad() has not been implemented.');
    }

    // tessellation primitives
    visitTriangleFan(triangleFan) {
      throw new Error('Method visitTriangleFan() has not been implemented.');
    }
    visitTriangleStrip(triangleStrip) {
      throw new Error('Method visitTriangleStrip() has not been implemented.');
    }
    visitQuadStrip(quadStrip) {
      throw new Error('Method visitQuadStrip() has not been implemented.');
    }
  }

  // requires testing
  class PrimitiveToPath2DConverter extends PrimitiveVisitor {
    path = new Path2D();
    strokeWeight;

    constructor({ strokeWeight }) {
      super();
      this.strokeWeight = strokeWeight;
    }

    // path primitives
    visitAnchor(anchor) {
      let vertex = anchor.getEndVertex();
      this.path.moveTo(vertex.position.x, vertex.position.y);
    }
    visitLineSegment(lineSegment) {
      if (lineSegment.isClosing) {
        // The same as lineTo, but it adds a stroke join between this
        // and the starting vertex rather than having two caps
        this.path.closePath();
      } else {
        let vertex = lineSegment.getEndVertex();
        this.path.lineTo(vertex.position.x, vertex.position.y);
      }
    }
    visitBezierSegment(bezierSegment) {
      let [v1, v2, v3] = bezierSegment.vertices;

      switch (bezierSegment.order) {
        case 2:
          this.path.quadraticCurveTo(
            v1.position.x,
            v1.position.y,
            v2.position.x,
            v2.position.y
          );
          break;
        case 3:
          this.path.bezierCurveTo(
            v1.position.x,
            v1.position.y,
            v2.position.x,
            v2.position.y,
            v3.position.x,
            v3.position.y
          );
          break;
      }
    }
    visitSplineSegment(splineSegment) {
      const shape = splineSegment._shape;

      if (
        splineSegment._splineProperties.ends === EXCLUDE &&
        !splineSegment._comesAfterSegment
      ) {
        let startVertex = splineSegment._firstInterpolatedVertex;
        this.path.moveTo(startVertex.position.x, startVertex.position.y);
      }

      const arrayVertices = splineSegment.getControlPoints().map(
        v => shape.vertexToArray(v)
      );
      let bezierArrays = shape.catmullRomToBezier(
        arrayVertices,
        splineSegment._splineProperties.tightness
      ).map(arr => arr.map(vertArr => shape.arrayToVertex(vertArr)));
      for (const array of bezierArrays) {
        const points = array.flatMap(vert => [vert.position.x, vert.position.y]);
        this.path.bezierCurveTo(...points);
      }
    }
    visitPoint(point) {
      const { x, y } = point.vertices[0].position;
      this.path.moveTo(x, y);
      // Hack: to draw just strokes and not fills, draw a very very tiny line
      this.path.lineTo(x + 0.00001, y);
    }
    visitLine(line) {
      const { x: x0, y: y0 } = line.vertices[0].position;
      const { x: x1, y: y1 } = line.vertices[1].position;
      this.path.moveTo(x0, y0);
      this.path.lineTo(x1, y1);
    }
    visitTriangle(triangle) {
      const [v0, v1, v2] = triangle.vertices;
      this.path.moveTo(v0.position.x, v0.position.y);
      this.path.lineTo(v1.position.x, v1.position.y);
      this.path.lineTo(v2.position.x, v2.position.y);
      this.path.closePath();
    }
    visitQuad(quad) {
      const [v0, v1, v2, v3] = quad.vertices;
      this.path.moveTo(v0.position.x, v0.position.y);
      this.path.lineTo(v1.position.x, v1.position.y);
      this.path.lineTo(v2.position.x, v2.position.y);
      this.path.lineTo(v3.position.x, v3.position.y);
      this.path.closePath();
    }
    visitTriangleFan(triangleFan) {
      const [v0, ...rest] = triangleFan.vertices;
      for (let i = 0; i < rest.length - 1; i++) {
        const v1 = rest[i];
        const v2 = rest[i + 1];
        this.path.moveTo(v0.position.x, v0.position.y);
        this.path.lineTo(v1.position.x, v1.position.y);
        this.path.lineTo(v2.position.x, v2.position.y);
        this.path.closePath();
      }
    }
    visitTriangleStrip(triangleStrip) {
      for (let i = 0; i < triangleStrip.vertices.length - 2; i++) {
        const v0 = triangleStrip.vertices[i];
        const v1 = triangleStrip.vertices[i + 1];
        const v2 = triangleStrip.vertices[i + 2];
        this.path.moveTo(v0.position.x, v0.position.y);
        this.path.lineTo(v1.position.x, v1.position.y);
        this.path.lineTo(v2.position.x, v2.position.y);
        this.path.closePath();
      }
    }
    visitQuadStrip(quadStrip) {
      for (let i = 0; i < quadStrip.vertices.length - 3; i += 2) {
        const v0 = quadStrip.vertices[i];
        const v1 = quadStrip.vertices[i + 1];
        const v2 = quadStrip.vertices[i + 2];
        const v3 = quadStrip.vertices[i + 3];
        this.path.moveTo(v0.position.x, v0.position.y);
        this.path.lineTo(v1.position.x, v1.position.y);
        // These are intentionally out of order to go around the quad
        this.path.lineTo(v3.position.x, v3.position.y);
        this.path.lineTo(v2.position.x, v2.position.y);
        this.path.closePath();
      }
    }
  }

  class PrimitiveToVerticesConverter extends PrimitiveVisitor {
    contours = [];
    curveDetail;
    pointsToLines;

    constructor({ curveDetail = 1, pointsToLines = true } = {}) {
      super();
      this.curveDetail = curveDetail;
      this.pointsToLines = pointsToLines;
    }

    lastContour() {
      return this.contours[this.contours.length - 1];
    }

    visitAnchor(anchor) {
      this.contours.push([]);
      // Weird edge case: if the next segment is a spline, we might
      // need to jump to a different vertex.
      const next = anchor._nextPrimitive;
      if (next?.canOverrideAnchor) {
        this.lastContour().push(next._firstInterpolatedVertex);
      } else {
        this.lastContour().push(anchor.getEndVertex());
      }
    }
    visitLineSegment(lineSegment) {
      this.lastContour().push(lineSegment.getEndVertex());
    }
    visitBezierSegment(bezierSegment) {
      const contour = this.lastContour();
      const numPoints = Math.max(
        1,
        Math.ceil(bezierSegment.hullLength() * this.curveDetail)
      );
      const vertexArrays = [
        bezierSegment.getStartVertex(),
        ...bezierSegment.vertices
      ].map(v => bezierSegment._shape.vertexToArray(v));
      for (let i = 0; i < numPoints; i++) {
        const t = (i + 1) / numPoints;
        contour.push(
          bezierSegment._shape.arrayToVertex(
            bezierSegment.order === 3
              ? bezierSegment._shape.evaluateCubicBezier(vertexArrays, t)
              : bezierSegment._shape.evaluateQuadraticBezier(vertexArrays, t)
          )
        );
      }
    }
    visitSplineSegment(splineSegment) {
      const shape = splineSegment._shape;
      const contour = this.lastContour();

      const arrayVertices = splineSegment.getControlPoints().map(
        v => shape.vertexToArray(v)
      );
      let bezierArrays = shape.catmullRomToBezier(
        arrayVertices,
        splineSegment._splineProperties.tightness
      );
      let startVertex = shape.vertexToArray(
        splineSegment._firstInterpolatedVertex
      );
      for (const array of bezierArrays) {
        const bezierControls = [startVertex, ...array];
        const numPoints = Math.max(
          1,
          Math.ceil(
            polylineLength(bezierControls.map(v => shape.arrayToVertex(v))) *
            this.curveDetail
          )
        );
        for (let i = 0; i < numPoints; i++) {
          const t = (i + 1) / numPoints;
          contour.push(
            shape.arrayToVertex(shape.evaluateCubicBezier(bezierControls, t))
          );
        }
        startVertex = array[2];
      }
    }
    visitPoint(point) {
      if (this.pointsToLines) {
        this.contours.push(...point.vertices.map(v => [v, v]));
      } else {
        this.contours.push(point.vertices.slice());
      }
    }
    visitLine(line) {
      this.contours.push(line.vertices.slice());
    }
    visitTriangle(triangle) {
      this.contours.push(triangle.vertices.slice());
    }
    visitQuad(quad) {
      this.contours.push(quad.vertices.slice());
    }
    visitTriangleFan(triangleFan) {
      // WebGL itself interprets the vertices as a fan, no reformatting needed
      this.contours.push(triangleFan.vertices.slice());
    }
    visitTriangleStrip(triangleStrip) {
      // WebGL itself interprets the vertices as a strip, no reformatting needed
      this.contours.push(triangleStrip.vertices.slice());
    }
    visitQuadStrip(quadStrip) {
      // WebGL itself interprets the vertices as a strip, no reformatting needed
      this.contours.push(quadStrip.vertices.slice());
    }
  }

  class PointAtLengthGetter extends PrimitiveVisitor {
    constructor() {
      super();
    }
  }

  function customShapes(p5, fn) {
    // ---- GENERAL CLASSES ----

    /**
     * @private
     * A class to describe a custom shape made with `beginShape()`/`endShape()`.
     *
     * Every `Shape` has a `kind`. The kind takes any value that
     * can be passed to <a href="#/p5/beginShape">beginShape()</a>:
     *
     * - `PATH`
     * - `POINTS`
     * - `LINES`
     * - `TRIANGLES`
     * - `QUADS`
     * - `TRIANGLE_FAN`
     * - `TRIANGLE_STRIP`
     * - `QUAD_STRIP`
     *
     * A `Shape` of any kind consists of `contours`, which can be thought of as
     * subshapes (shapes inside another shape). Each `contour` is built from
     * basic shapes called primitives, and each primitive consists of one or more vertices.
     *
     * For example, a square can be made from a single path contour with four line-segment
     * primitives. Each line segment contains a vertex that indicates its endpoint. A square
     * with a circular hole in it contains the circle in a separate contour.
     *
     * By default, each vertex only has a position, but a shape's vertices may have other
     * properties such as texture coordinates, a normal vector, a fill color, and a stroke color.
     * The properties every vertex should have may be customized by passing `vertexProperties` to
     * `createShape()`.
     *
     * Once a shape is created and given a name like `myShape`, it can be built up with
     * methods such as `myShape.beginShape()`, `myShape.vertex()`, and `myShape.endShape()`.
     *
     * Vertex functions such as `vertex()` or `bezierVertex()` are used to set the `position`
     * property of vertices, as well as the `textureCoordinates` property if applicable. Those
     * properties only apply to a single vertex.
     *
     * If `vertexProperties` includes other properties, they are each set by a method of the
     * same name. For example, if vertices in `myShape` have a `fill`, then that is set with
     * `myShape.fill()`. In the same way that a <a href="#/p5/fill">fill()</a> may be applied
     * to one or more shapes, `myShape.fill()` may be applied to one or more vertices.
     *
     * @class p5.Shape
     * @param {Object} [vertexProperties={position: createVector(0, 0)}] vertex properties and their initial values.
     */

    p5.Shape = Shape;

    /**
     * @private
     * A class to describe a contour made with `beginContour()`/`endContour()`.
     *
     * Contours may be thought of as shapes inside of other shapes.
     * For example, a contour may be used to create a hole in a shape that is created
     * with <a href="#/p5/beginShape">beginShape()</a>/<a href="#/p5/endShape">endShape()</a>.
     * Multiple contours may be included inside a single shape.
     *
     * Contours can have any `kind` that a shape can have:
     *
     * - `PATH`
     * - `POINTS`
     * - `LINES`
     * - `TRIANGLES`
     * - `QUADS`
     * - `TRIANGLE_FAN`
     * - `TRIANGLE_STRIP`
     * - `QUAD_STRIP`
     *
     * By default, a contour has the same kind as the shape that contains it, but this
     * may be changed by passing a different `kind` to <a href="#/p5/beginContour">beginContour()</a>.
     *
     * A `Contour` of any kind consists of `primitives`, which are the most basic
     * shapes that can be drawn. For example, if a contour is a hexagon, then
     * it's made from six line-segment primitives.
     *
     * @class p5.Contour
     */

    p5.Contour = Contour;

    /**
     * @private
     * A base class to describe a shape primitive (a basic shape drawn with
     * `beginShape()`/`endShape()`).
     *
     * Shape primitives are the most basic shapes that can be drawn with
     * <a href="#/p5/beginShape">beginShape()</a>/<a href="#/p5/endShape">endShape()</a>:
     *
     * - segment primitives: line segments, bezier segments, spline segments, and arc segments
     * - isolated primitives: points, lines, triangles, and quads
     * - tessellation primitives: triangle fans, triangle strips, and quad strips
     *
     * More complex shapes may be created by combining many primitives, possibly of different kinds,
     * into a single shape.
     *
     * In a similar way, every shape primitive is built from one or more vertices.
     * For example, a point consists of a single vertex, while a triangle consists of three vertices.
     * Each type of shape primitive has a `vertexCapacity`, which may be `Infinity` (for example, a
     * spline may consist of any number of vertices). A primitive's `vertexCount` is the number of
     * vertices it currently contains.
     *
     * Each primitive can add itself to a shape with an `addToShape()` method.
     *
     * It can also accept visitor objects with an `accept()` method. When a primitive accepts a visitor,
     * it gives the visitor access to its vertex data. For example, one visitor to a segment might turn
     * the data into 2D drawing instructions. Another might find a point at a given distance
     * along the segment.
     *
     * @class p5.ShapePrimitive
     * @abstract
     */

    p5.ShapePrimitive = ShapePrimitive;

    /**
     * @private
     * A class to describe a vertex (a point on a shape), in 2D or 3D.
     *
     * Vertices are the basic building blocks of all `p5.Shape` objects, including
     * shapes made with <a href="#/p5/vertex">vertex()</a>, <a href="#/p5/arcVertex">arcVertex()</a>,
     * <a href="#/p5/bezierVertex">bezierVertex()</a>, and <a href="#/p5/splineVertex">splineVertex()</a>.
     *
     * Like a point on an object in the real world, a vertex may have different properties.
     * These may include coordinate properties such as `position`, `textureCoordinates`, and `normal`,
     * color properties such as `fill` and `stroke`, and more.
     *
     * A vertex called `myVertex` with position coordinates `(2, 3, 5)` and a green stroke may be created
     * like this:
     *
     * ```js
     * let myVertex = new p5.Vertex({
     *   position: createVector(2, 3, 5),
     *   stroke: color('green')
     * });
     * ```
     *
     * Any property names may be used. The `p5.Shape` class assumes that if a vertex has a
     * position or texture coordinates, they are stored in `position` and `textureCoordinates`
     * properties.
     *
     * Property values may be any
     * <a href="https://developer.mozilla.org/en-US/docs/Glossary/Primitive">JavaScript primitive</a>, any
     * <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer">object literal</a>,
     * or any object with an `array` property.
     *
     * For example, if a position is stored as a `p5.Vector` object and a stroke is stored as a `p5.Color` object,
     * then the `array` properties of those objects will be used by the vertex's own `array` property, which provides
     * all the vertex data in a single array.
     *
     * @class p5.Vertex
     * @param {Object} [properties={position: createVector(0, 0)}] vertex properties.
     */

    p5.Vertex = Vertex;

    // ---- PATH PRIMITIVES ----

    /**
     * @private
     * A class responsible for...
     *
     * @class p5.Anchor
     * @extends p5.ShapePrimitive
     * @param {p5.Vertex} vertex the vertex to include in the anchor.
     */

    p5.Anchor = Anchor;

    /**
     * @private
     * A class responsible for...
     *
     * Note: When a segment is added to a shape, it's attached to an anchor or another segment.
     * Adding it to another shape may result in unexpected behavior.
     *
     * @class p5.Segment
     * @extends p5.ShapePrimitive
     * @param {...p5.Vertex} vertices the vertices to include in the segment.
     */

    p5.Segment = Segment;

    /**
     * @private
     * A class responsible for...
     *
     * @class p5.LineSegment
     * @param {p5.Vertex} vertex the vertex to include in the anchor.
     */

    p5.LineSegment = LineSegment;

    /**
     * @private
     * A class responsible for...
     */

    p5.BezierSegment = BezierSegment$1;

    /**
     * @private
     * A class responsible for...
     */

    p5.SplineSegment = SplineSegment;

    // ---- ISOLATED PRIMITIVES ----

    /**
     * @private
     * A class responsible for...
     */

    p5.Point = Point;

    /**
     * @private
     * A class responsible for...
     *
     * @class p5.Line
     * @param {...p5.Vertex} vertices the vertices to include in the line.
     */

    p5.Line = Line;

    /**
     * @private
     * A class responsible for...
     */

    p5.Triangle = Triangle;

    /**
     * @private
     * A class responsible for...
     */

    p5.Quad = Quad;

    // ---- TESSELLATION PRIMITIVES ----

    /**
     * @private
     * A class responsible for...
     */

    p5.TriangleFan = TriangleFan;

    /**
     * @private
     * A class responsible for...
     */

    p5.TriangleStrip = TriangleStrip;

    /**
     * @private
     * A class responsible for...
     */

    p5.QuadStrip = QuadStrip;

    // ---- PRIMITIVE VISITORS ----

    /**
     * @private
     * A class responsible for...
     */

    p5.PrimitiveVisitor = PrimitiveVisitor;

    /**
     * @private
     * A class responsible for...
     *
     * Notes:
     * 1. Assumes vertex positions are stored as p5.Vector instances.
     * 2. Currently only supports position properties of vectors.
     */

    p5.PrimitiveToPath2DConverter = PrimitiveToPath2DConverter;

    /**
     * @private
     * A class responsible for...
     */

    p5.PrimitiveToVerticesConverter = PrimitiveToVerticesConverter;

    /**
     * @private
     * A class responsible for...
     */

    p5.PointAtLengthGetter = PointAtLengthGetter;

    // ---- FUNCTIONS ----


    /**
     * Influences the shape of the Bézier curve segment in a custom shape.
     * By default, this is 3; the other possible parameter is 2. This
     * results in quadratic Bézier curves.
     *
     * `bezierVertex()` adds a curved segment to custom shapes. The Bézier curves
     * it creates are defined like those made by the
     * <a href="#/p5/bezier">bezier()</a> function. `bezierVertex()` must be
     * called between the
     * <a href="#/p5/beginShape">beginShape()</a> and
     * <a href="#/p5/endShape">endShape()</a> functions. There must be at least
     * one call to <a href="#/p5/vertex">bezierVertex()</a>, before
     * a number of `bezierVertex()` calls that is a multiple of the parameter
     * set by <a href="#/p5/bezierOrder">bezierOrder(...)</a> (default 3).
     *
     * Each curve of order 3 requires three calls to `bezierVertex`, so
     * 2 curves would need 7 calls to `bezierVertex()`:
     * (1 one initial anchor point, two sets of 3 curves describing the curves)
     * With `bezierOrder(2)`, two curves would need 5 calls: 1 + 2 + 2.
     *
     * Bézier curves can also be drawn in 3D using WebGL mode.
     *
     * Note: `bezierVertex()` won’t work when an argument is passed to
     * <a href="#/p5/beginShape">beginShape()</a>.
     *
     * @method bezierOrder
     * @param {Number} order The new order to set. Can be either 2 or 3, by default 3
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the shape.
     *   noFill();
     *
     *   // Start drawing the shape.
     *   beginShape();
     *
     *   // set the order to 2 for a quadratic Bézier curve
     *   bezierOrder(2);
     *
     *   // Add the first anchor point.
     *   bezierVertex(30, 20);
     *
     *   // Add the Bézier vertex.
     *   bezierVertex(80, 20);
     *   bezierVertex(50, 50);
     *
     *   // Stop drawing the shape.
     *   endShape();
     *
     *   describe('A black curve drawn on a gray square. The curve starts at the top-left corner and ends at the center.');
     * }
     */
    /**
     * @method bezierOrder
     * @returns {Number} The current Bézier order.
     */
    fn.bezierOrder = function(order) {
      return this._renderer.bezierOrder(order);
    };



    /**
     * Connects points with a smooth curve (a spline).
     *
     * `splineVertex()` adds a curved segment to custom shapes.
     * The curve it creates follows the same rules as the ones
     * made with the <a href="#/p5/spline">spline()</a> function.
     * `splineVertex()` must be called between the
     * <a href="#/p5/beginShape">beginShape()</a> and
     * <a href="#/p5/endShape">endShape()</a> functions.
     *
     * Spline curves can form shapes and curves that slope gently. They’re like
     * cables that are attached to a set of points. `splineVertex()` draws a smooth
     * curve through the points you give it.
     * <a href="#/p5/beginShape">beginShape()</a> and
     * <a href="#/p5/endShape">endShape()</a> in order to draw a curve:
     *
     *
     * If you provide three points, the spline will pass through them.
     * It works the same way with any number of points.
     *
     *
     *
     * ```js
     * beginShape();
     *
     * // Add the first point.
     * splineVertex(25, 80);
     *
     * // Add the second point.
     * splineVertex(20, 30);
     *
     * // Add the last point.
     * splineVertex(85, 60);
     *
     * endShape();
     * ```
     *
     * <img src="assets/openCurveSpline.png"></img>
     *
     *
     * Passing in `CLOSE` to `endShape()` closes the spline smoothly.
     * ```js
     * beginShape();
     *
     * // Add the first point.
     * splineVertex(25, 80);
     *
     * // Add the second point.
     * splineVertex(20, 30);
     *
     * // Add the second point.
     * splineVertex(85, 60);
     *
     * endShape(CLOSE);
     * ```
     *
     * <img src="assets/closeCurveSpline.png"></img>
     *
     *
     * By default (`ends: INCLUDE`), the curve passes through
     * all the points you add with `splineVertex()`, similar to
     * the <a href="#/p5/spline">spline()</a> function. To draw only
     * the middle span p1->p2 (skipping p0->p1 and p2->p3), set
     * `splineProperty('ends', EXCLUDE)`. You don’t need to duplicate
     * vertices to draw those spans.
     *
     * Spline curves can also be drawn in 3D using WebGL mode. The 3D version of
     * `splineVertex()` has three arguments because each point has x-, y-, and
     * z-coordinates. By default, the vertex’s z-coordinate is set to 0.
     *
     * Note: `splineVertex()` won’t work when an argument is passed to
     * <a href="#/p5/beginShape">beginShape()</a>.
     *
     * @method splineVertex
     * @param {Number} x x-coordinate of the vertex
     * @param {Number} y y-coordinate of the vertex
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(220);
     *   noFill();
     *   strokeWeight(1);
     *
     *   beginShape();
     *   splineVertex(25, 80);
     *   splineVertex(20, 30);
     *   splineVertex(85, 60);
     *   endShape();
     *
     *   strokeWeight(5);
     *   stroke(0);
     *
     *   point(25, 80);
     *   point(20, 30);
     *   point(85, 60);
     *
     *   describe(
     *     'On a gray background, a black spline passes through three marked points.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *   background(220);
     *
     *   beginShape();
     *   splineVertex(25, 80);
     *   splineVertex(20, 30);
     *   splineVertex(85, 60);
     *   endShape(CLOSE);
     *
     *   describe(
     *     'On a gray background, a closed black spline with a white interior forms a triangular shape with smooth corners.'
     *   );
     * }
     *
     * @example
     * let ringInnerRadius, ringWidth;
     * let radius, dRadius;
     * let theta, dTheta;
     * let time, dTime;
     * let vertexCount, unit, offset;
     *
     * function setup() {
     *   createCanvas(400, 400);
     *
     *   vertexCount = 15;
     *   unit = createVector(1, 0);
     *   dTheta = TAU / vertexCount;
     *   dTime = 0.004;
     *
     *   ringInnerRadius = 25;
     *   ringWidth = 5 * ringInnerRadius;
     *
     *   offset = width;
     *
     *   describe(
     *     'A white blob with a black outline changes its shape over time.'
     *   );
     * }
     *
     * function draw() {
     *   background(220);
     *   strokeWeight(2);
     *   translate(width / 2, height / 2);
     *
     *   time = dTime * frameCount;
     *
     *   beginShape();
     *   for (let i = 0; i < vertexCount; i++) {
     *     unit.rotate(dTheta);
     *     dRadius = noise(offset + unit.x, offset + unit.y, time) * ringWidth;
     *     radius = ringInnerRadius + dRadius;
     *     splineVertex(radius * unit.x, radius * unit.y);
     *   }
     *   endShape(CLOSE);
     * }
     *
     * @example
     * let vertexA;
     * let vertexB;
     * let vertexC;
     * let vertexD;
     * let vertexE;
     * let vertexF;
     *
     * let markerRadius;
     *
     * let vectorAB;
     * let vectorFE;
     *
     * let endOfTangentB;
     * let endOfTangentE;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Initialize variables
     *   // Adjusting vertices A and F affects the slopes at B and E
     *
     *   vertexA = createVector(35, 85);
     *   vertexB = createVector(25, 70);
     *   vertexC = createVector(30, 30);
     *   vertexD = createVector(70, 30);
     *   vertexE = createVector(75, 70);
     *   vertexF = createVector(65, 85);
     *
     *   markerRadius = 4;
     *
     *   vectorAB = p5.Vector.sub(vertexB, vertexA);
     *   vectorFE = p5.Vector.sub(vertexE, vertexF);
     *
     *   endOfTangentB = p5.Vector.add(vertexC, vectorAB);
     *   endOfTangentE = p5.Vector.add(vertexD, vectorFE);
     *
     *   splineProperty(`ends`, EXCLUDE);
     *
     *   // Draw figure
     *
     *   background(220);
     *
     *   noFill();
     *
     *   beginShape();
     *   splineVertex(vertexA.x, vertexA.y);
     *   splineVertex(vertexB.x, vertexB.y);
     *   splineVertex(vertexC.x, vertexC.y);
     *   splineVertex(vertexD.x, vertexD.y);
     *   splineVertex(vertexE.x, vertexE.y);
     *   splineVertex(vertexF.x, vertexF.y);
     *   endShape();
     *
     *   stroke('red');
     *   line(vertexA.x, vertexA.y, vertexC.x, vertexC.y);
     *   line(vertexB.x, vertexB.y, endOfTangentB.x, endOfTangentB.y);
     *
     *   stroke('blue');
     *   line(vertexD.x, vertexD.y, vertexF.x, vertexF.y);
     *   line(vertexE.x, vertexE.y, endOfTangentE.x, endOfTangentE.y);
     *
     *   fill('white');
     *   stroke('black');
     *   circle(vertexA.x, vertexA.y, markerRadius);
     *   circle(vertexB.x, vertexB.y, markerRadius);
     *   circle(vertexC.x, vertexC.y, markerRadius);
     *   circle(vertexD.x, vertexD.y, markerRadius);
     *   circle(vertexE.x, vertexE.y, markerRadius);
     *   circle(vertexF.x, vertexF.y, markerRadius);
     *
     *   fill('black');
     *   noStroke();
     *   text('A', vertexA.x - 15, vertexA.y + 5);
     *   text('B', vertexB.x - 15, vertexB.y + 5);
     *   text('C', vertexC.x - 5, vertexC.y - 5);
     *   text('D', vertexD.x - 5, vertexD.y - 5);
     *   text('E', vertexE.x + 5, vertexE.y + 5);
     *   text('F', vertexF.x + 5, vertexF.y + 5);
     *
     *   describe('On a gray background, a black spline passes through vertices A, B, C, D, E, and F, shown as white circles. A red line segment joining vertices A and C has the same slope as the red tangent segment at B. Similarly, the blue line segment joining vertices D and F has the same slope as the blue tangent at E.');
     * }
     */
    /**
     * @method splineVertex
     * @param {Number} x
     * @param {Number} y
     * @param {Number} [z] z-coordinate of the vertex.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A ghost shape drawn in white on a blue background. When the user drags the mouse, the scene rotates to reveal the outline of a second ghost.');
     * }
     *
     * function draw() {
     *   background('midnightblue');
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the first ghost.
     *   noStroke();
     *   fill('ghostwhite');
     *
     *   beginShape();
     *   splineVertex(-28, 41, 0);
     *   splineVertex(-28, 41, 0);
     *   splineVertex(-29, -33, 0);
     *   splineVertex(18, -31, 0);
     *   splineVertex(34, 41, 0);
     *   splineVertex(34, 41, 0);
     *   endShape();
     *
     *   // Draw the second ghost.
     *   noFill();
     *   stroke('ghostwhite');
     *
     *   beginShape();
     *   splineVertex(-28, 41, -20);
     *   splineVertex(-28, 41, -20);
     *   splineVertex(-29, -33, -20);
     *   splineVertex(18, -31, -20);
     *   splineVertex(34, 41, -20);
     *   splineVertex(34, 41, -20);
     *   endShape();
     * }
     */
    /**
     * @method splineVertex
     * @param {Number} x
     * @param {Number} y
     * @param {Number} [u=0]
     * @param {Number} [v=0]
     */
    /**
     * @method splineVertex
     * @param {Number} x
     * @param {Number} y
     * @param {Number} z
     * @param {Number} [u=0]
     * @param {Number} [v=0]
     */
    fn.splineVertex = function(...args) {
      let x = 0, y = 0, z = 0, u = 0, v = 0;
      if (args.length === 2) {
        [x, y] = args;
      } else if (args.length === 4) {
        [x, y, u, v] = args;
      } else if (args.length === 3) {
        [x, y, z] = args;
      } else if (args.length === 5) {
        [x, y, z, u, v] = args;
      }
      this._renderer.splineVertex(x, y, z, u, v);
    };

    /**
     * Gets or sets a given spline property.
     *
     * Use `splineProperty()` to adjust the behavior of splines
     * created with `splineVertex()` or `spline()`. You can control
     * two key aspects of a spline: its end behavior (`ends`) and
     * its curvature (`tightness`).
     *
     * By default, the ends property is set to `INCLUDE`, which means
     * the spline passes through every point, including the endpoints.
     * You can also set it to `EXCLUDE` i.e. `splineProperty('ends', EXCLUDE)`,
     * which makes the spline pass through all points except the endpoints.
     *
     * `INCLUDE` case will have the spline passing through
     * all points, like this:
     *
     * ```js
     * splineProperty('ends', INCLUDE); // no need to set this, as it is the default
     * spline(25, 46, 93, 44, 93, 81, 35, 85);
     *
     * point(25, 46);
     * point(93, 44);
     * point(93, 81);
     * point(35, 85);
     * ```
     *
     * <img src="assets/includeSpline.png"></img>
     *
     *
     * EXCLUDE case will have the spline passing through
     * the middle points, like this:
     *
     *
     * ```js
     * splineProperty('ends', EXCLUDE);
     * spline(25, 46, 93, 44, 93, 81, 35, 85);
     *
     * point(25, 46);
     * point(93, 44);
     * point(93, 81);
     * point(35, 85);
     * ```
     *
     * <img src="assets/excludeSpline.png"></img>
     *
     * By default, the tightness property is set to `0`,
     * producing a smooth curve that passes evenly through
     * the vertices. Negative values make the curve looser,
     * while positive values make it tighter. Common values
     * range between -1 and 1, though values outside this
     * range can also be used for different effects.
     *
     * For example, To set tightness, use `splineProperty('tightness', t)`,
     * (default: t = 0).
     *
     * Here's the example showing negetive value of tightness,
     * which creates a rounder bulge:
     *
     * ```js
     * splineProperty('tightness', -5)
     * stroke(0);
     * strokeWeight(2);
     * spline(25, 46, 93, 44, 93, 81, 35, 85);
     * ```
     * <img src="assets/roundBulge.png"></img>
     * Here's the example showing positive value of tightness,
     * which makes the curve tighter and more angular:
     *
     * ```js
     * splineProperty('tightness', 5)
     * stroke(0);
     * strokeWeight(2);
     * spline(25, 46, 93, 44, 93, 81, 35, 85);
     * ```
     * <img src="assets/anglurBulge.png"></img>
     *
     * In all cases, the splines in p5.js are <a href = "https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Cardinal_spline">cardinal splines</a>.
     * When tightness is 0, these splines are often known as
     * <a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Catmull%E2%80%93Rom_spline">Catmull-Rom splines</a>
     *
     * @method splineProperty
     * @param {String} property
     * @param value Value to set the given property to.
     *
     * @example
     * // Move the mouse left and right to see the curve change.
     *
     * let t;
     *
     * function setup() {
     *   createCanvas(100, 100);
     * }
     *
     * function draw() {
     *   background(240);
     *
     *   t = map(mouseX, 0, width, -5, 5, true);
     *   splineProperty('tightness', t);
     *
     *   noFill();
     *   stroke(0);
     *   strokeWeight(2);
     *
     *   beginShape();
     *   splineVertex(10, 26);
     *   splineVertex(83, 24);
     *
     *   splineVertex(83, 61);
     *   splineVertex(25, 65);
     *   endShape();
     *
     *   push();
     *   strokeWeight(5);
     *   point(10, 26);
     *   point(83, 24);
     *   point(83, 61);
     *   point(25, 65);
     *   pop();
     *
     *   fill(0);
     *   noStroke();
     *   textSize(10);
     *   text(`tightness: ${round(t, 1)}`, 15, 90);
     *   describe('A black spline forms a sideways U shape through four points. The spline passes through the points more loosely as the mouse moves left of center (negative tightness), and more tightly as it moves right of center (positive tightness). The tightness is displayed at the bottom.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(360, 140);
     *   background(240);
     *   noFill();
     *
     *   // Right panel: ends = INCLUDE (all spans).
     *   push();
     *   translate(10, 10);
     *   stroke(220);
     *   rect(0, 0, 160, 120);
     *   fill(30);
     *   textSize(11);
     *   text('ends: INCLUDE (all spans)', 8, 16);
     *   noFill();
     *
     *   splineProperty('ends', INCLUDE);
     *   stroke(0);
     *   strokeWeight(2);
     *   spline(25, 46, 93, 44, 93, 81, 35, 85);
     *
     *   // vertices
     *   strokeWeight(5);
     *   stroke(0);
     *   point(25, 46);
     *   point(93, 44);
     *   point(93, 81);
     *   point(35, 85);
     *   pop();
     *
     *   // Right panel: ends = EXCLUDE (middle only).
     *   push();
     *   translate(190, 10);
     *   stroke(220);
     *   rect(0, 0, 160, 120);
     *   noStroke();
     *   fill(30);
     *   text('ends: EXCLUDE ', 18, 16);
     *   noFill();
     *
     *   splineProperty('ends', EXCLUDE);
     *   stroke(0);
     *   strokeWeight(2);
     *   spline(25, 46, 93, 44, 93, 81, 35, 85);
     *
     *   // vertices
     *   strokeWeight(5);
     *   stroke(0);
     *   point(25, 46);
     *   point(93, 44);
     *   point(93, 81);
     *   point(35, 85);
     *   pop();
     *
     *   describe('Left panel shows spline with ends INCLUDE (three spans). Right panel shows EXCLUDE (only the middle span). Four black points mark the vertices.');
     * }
     *
     * @example
     * let vertexA;
     * let vertexB;
     * let vertexC;
     * let vertexD;
     * let vertexE;
     * let vertexF;
     *
     * let markerRadius;
     *
     * let vectorAB;
     * let vectorFE;
     *
     * let endOfTangentB;
     * let endOfTangentE;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Initialize variables
     *   // Adjusting vertices A and F affects the slopes at B and E
     *
     *   vertexA = createVector(35, 85);
     *   vertexB = createVector(25, 70);
     *   vertexC = createVector(30, 30);
     *   vertexD = createVector(70, 30);
     *   vertexE = createVector(75, 70);
     *   vertexF = createVector(65, 85);
     *
     *   markerRadius = 4;
     *
     *   vectorAB = p5.Vector.sub(vertexB, vertexA);
     *   vectorFE = p5.Vector.sub(vertexE, vertexF);
     *
     *   endOfTangentB = p5.Vector.add(vertexC, vectorAB);
     *   endOfTangentE = p5.Vector.add(vertexD, vectorFE);
     *
     *   splineProperty(`ends`, EXCLUDE);
     *
     *   // Draw figure
     *
     *   background(220);
     *
     *   noFill();
     *
     *   beginShape();
     *   splineVertex(vertexA.x, vertexA.y);
     *   splineVertex(vertexB.x, vertexB.y);
     *   splineVertex(vertexC.x, vertexC.y);
     *   splineVertex(vertexD.x, vertexD.y);
     *   splineVertex(vertexE.x, vertexE.y);
     *   splineVertex(vertexF.x, vertexF.y);
     *   endShape();
     *
     *   stroke('red');
     *   line(vertexA.x, vertexA.y, vertexC.x, vertexC.y);
     *   line(vertexB.x, vertexB.y, endOfTangentB.x, endOfTangentB.y);
     *
     *   stroke('blue');
     *   line(vertexD.x, vertexD.y, vertexF.x, vertexF.y);
     *   line(vertexE.x, vertexE.y, endOfTangentE.x, endOfTangentE.y);
     *
     *   fill('white');
     *   stroke('black');
     *   circle(vertexA.x, vertexA.y, markerRadius);
     *   circle(vertexB.x, vertexB.y, markerRadius);
     *   circle(vertexC.x, vertexC.y, markerRadius);
     *   circle(vertexD.x, vertexD.y, markerRadius);
     *   circle(vertexE.x, vertexE.y, markerRadius);
     *   circle(vertexF.x, vertexF.y, markerRadius);
     *
     *   fill('black');
     *   noStroke();
     *   text('A', vertexA.x - 15, vertexA.y + 5);
     *   text('B', vertexB.x - 15, vertexB.y + 5);
     *   text('C', vertexC.x - 5, vertexC.y - 5);
     *   text('D', vertexD.x - 5, vertexD.y - 5);
     *   text('E', vertexE.x + 5, vertexE.y + 5);
     *   text('F', vertexF.x + 5, vertexF.y + 5);
     *
     *   describe('On a gray background, a black spline passes through vertices A, B, C, D, E, and F, shown as white circles. A red line segment joining vertices A and C has the same slope as the red tangent segment at B. Similarly, the blue line segment joining vertices D and F has the same slope as the blue tangent at E.');
     * }
     */
    /**
     * @method splineProperty
     * @param {String} property
     * @returns The current value for the given property.
     */
    fn.splineProperty = function(property, value) {
      return this._renderer.splineProperty(property, value);
    };

    /**
     * Sets multiple properties for spline curves at once.
     *
     * `splineProperties()` accepts an object with key-value pairs to configure
     * how spline curves are drawn. This is a convenient way to set multiple
     * spline properties with a single function call, rather than calling
     * <a href="#/p5/splineProperty">splineProperty()</a> multiple times.
     *
     * The properties object can include:
     * - `tightness`: A number that controls how tightly the curve fits to the
     *   vertex points. The default value is 0. Positive values make the curve
     *   tighter (straighter), while negative values make it looser. Values
     *   between -5 and 5 work best.
     * - `ends`: Controls whether to draw the end segments of the spline. Set to
     *   `EXCLUDE` to skip drawing the segments between the first and second
     *   points and between the second-to-last and last points. This is useful
     *   when you want to use the first and last points as control points only.
     *
     * `splineProperties()` affects curves drawn with
     * <a href="#/p5/splineVertex">splineVertex()</a> within
     * <a href="#/p5/beginShape">beginShape()</a> and
     * <a href="#/p5/endShape">endShape()</a>, as well as curves drawn with
     * <a href="#/p5/spline">spline()</a>. The properties remain active until
     * changed by another call to `splineProperties()` or
     * <a href="#/p5/splineProperty">splineProperty()</a>.
     *
     * @method splineProperties
     * @param  {Object} values an object containing spline property key-value pairs
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *   background(220);
     *
     *   // Set spline tightness using splineProperties
     *   splineProperties({
     *     tightness: 0.5
     *   });
     *
     *   // Draw a spline curve
     *   noFill();
     *   stroke(0);
     *   strokeWeight(2);
     *
     *   beginShape();
     *   splineVertex(20, 80);
     *   splineVertex(30, 30);
     *   splineVertex(70, 30);
     *   splineVertex(80, 80);
     *   endShape();
     *
     *   // Show vertex points
     *   fill(255, 0, 0);
     *   noStroke();
     *   circle(20, 80, 6);
     *   circle(30, 30, 6);
     *   circle(70, 30, 6);
     *   circle(80, 80, 6);
     *
     *   describe('A smooth curved line with tightness 0.5 connecting four red points.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *   background(220);
     *
     *   // Exclude end segments - first and last points become control points
     *   splineProperties({
     *     tightness: 0,
     *     ends: EXCLUDE
     *   });
     *
     *   // Draw curve only between middle points
     *   noFill();
     *   stroke(0);
     *   strokeWeight(2);
     *
     *   beginShape();
     *   splineVertex(10, 50);  // Control point (affects curve but not drawn to)
     *   splineVertex(30, 20);  // Start of visible curve
     *   splineVertex(70, 80);  // End of visible curve
     *   splineVertex(90, 50);  // Control point (affects curve but not drawn to)
     *   endShape();
     *
     *   // Show all points
     *   fill(200, 0, 0);
     *   noStroke();
     *   circle(10, 50, 6);  // Control point
     *   circle(90, 50, 6);  // Control point
     *
     *   fill(0, 0, 255);
     *   circle(30, 20, 6);  // Visible curve point
     *   circle(70, 80, 6);  // Visible curve point
     *
     *   describe('A curved line between two blue points, with red control points at the ends.');
     * }
     *
     * @method splineProperties
     * @return {Object}
     */
    fn.splineProperties = function(values) {
      return this._renderer.splineProperties(values);
    };

    /**
     * Adds a vertex to a custom shape.
     *
     * `vertex()` sets the coordinates of vertices drawn between the
     * <a href="#/p5/beginShape">beginShape()</a> and
     * <a href="#/p5/endShape">endShape()</a> functions.
     *
     * The first two parameters, `x` and `y`, set the x- and y-coordinates of the
     * vertex.
     *
     * The third parameter, `z`, is optional. It sets the z-coordinate of the
     * vertex in WebGL mode. By default, `z` is 0.
     *
     * The fourth and fifth parameters, `u` and `v`, are also optional. They set
     * the u- and v-coordinates for the vertex’s texture when used with
     * <a href="#/p5/endShape">endShape()</a>. By default, `u` and `v` are both 0.
     *
     * @method vertex
     * @param  {Number} x x-coordinate of the vertex.
     * @param  {Number} y y-coordinate of the vertex.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the shape.
     *   strokeWeight(3);
     *
     *   // Start drawing the shape.
     *   // Only draw the vertices.
     *   beginShape(POINTS);
     *
     *   // Add the vertices.
     *   vertex(30, 20);
     *   vertex(85, 20);
     *   vertex(85, 75);
     *   vertex(30, 75);
     *
     *   // Stop drawing the shape.
     *   endShape();
     *
     *   describe('Four black dots that form a square are drawn on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Start drawing the shape.
     *   beginShape();
     *
     *   // Add vertices.
     *   vertex(30, 20);
     *   vertex(85, 20);
     *   vertex(85, 75);
     *   vertex(30, 75);
     *
     *   // Stop drawing the shape.
     *   endShape(CLOSE);
     *
     *   describe('A white square on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   // Start drawing the shape.
     *   beginShape();
     *
     *   // Add vertices.
     *   vertex(-20, -30, 0);
     *   vertex(35, -30, 0);
     *   vertex(35, 25, 0);
     *   vertex(-20, 25, 0);
     *
     *   // Stop drawing the shape.
     *   endShape(CLOSE);
     *
     *   describe('A white square on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white square spins around slowly on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Rotate around the y-axis.
     *   rotateY(frameCount * 0.01);
     *
     *   // Start drawing the shape.
     *   beginShape();
     *
     *   // Add vertices.
     *   vertex(-20, -30, 0);
     *   vertex(35, -30, 0);
     *   vertex(35, 25, 0);
     *   vertex(-20, 25, 0);
     *
     *   // Stop drawing the shape.
     *   endShape(CLOSE);
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load an image to apply as a texture.
     *   img = await loadImage('assets/laDefense.jpg');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A photograph of a ceiling rotates slowly against a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Rotate around the y-axis.
     *   rotateY(frameCount * 0.01);
     *
     *   // Style the shape.
     *   noStroke();
     *
     *   // Apply the texture.
     *   texture(img);
     *   textureMode(NORMAL);
     *
     *   // Start drawing the shape
     *   beginShape();
     *
     *   // Add vertices.
     *   vertex(-20, -30, 0, 0, 0);
     *   vertex(35, -30, 0, 1, 0);
     *   vertex(35, 25, 0, 1, 1);
     *   vertex(-20, 25, 0, 0, 1);
     *
     *   // Stop drawing the shape.
     *   endShape();
     * }
     *
     * @example
     * let vid;
     * function setup() {
     *   // Load a video and create a p5.MediaElement object.
     *   vid = createVideo('/assets/fingers.mov');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Hide the video.
     *   vid.hide();
     *
     *   // Set the video to loop.
     *   vid.loop();
     *
     *   describe('A rectangle with video as texture');
     * }
     *
     * function draw() {
     *   background(0);
     *
     *   // Rotate around the y-axis.
     *   rotateY(frameCount * 0.01);
     *
     *   // Set the texture mode.
     *   textureMode(NORMAL);
     *
     *   // Apply the video as a texture.
     *   texture(vid);
     *
     *   // Draw a custom shape using uv coordinates.
     *   beginShape();
     *   vertex(-40, -40, 0, 0);
     *   vertex(40, -40, 1, 0);
     *   vertex(40, 40, 1, 1);
     *   vertex(-40, 40, 0, 1);
     *   endShape();
     * }
     */
    /**
     * @method vertex
     * @param  {Number} x
     * @param  {Number} y
     * @param  {Number} [u=0]   u-coordinate of the vertex's texture.
     * @param  {Number} [v=0]   v-coordinate of the vertex's texture.
     */
    /**
     * @method vertex
     * @param  {Number} x
     * @param  {Number} y
     * @param  {Number} z
     * @param  {Number} [u=0]   u-coordinate of the vertex's texture.
     * @param  {Number} [v=0]   v-coordinate of the vertex's texture.
     */
    fn.vertex = function(x, y) {
      let z, u, v;

      // default to (x, y) mode: all other arguments assumed to be 0.
      z = u = v = 0;

      if (arguments.length === 3) {
        // (x, y, z) mode: (u, v) assumed to be 0.
        z = arguments[2];
      } else if (arguments.length === 4) {
        // (x, y, u, v) mode: z assumed to be 0.
        u = arguments[2];
        v = arguments[3];
      } else if (arguments.length === 5) {
        // (x, y, z, u, v) mode
        z = arguments[2];
        u = arguments[3];
        v = arguments[4];
      }
      this._renderer.vertex(x, y, z, u, v);
      return;
    };

    /**
     * Begins creating a hole within a flat shape.
     *
     * The `beginContour()` and <a href="#/p5/endContour">endContour()</a>
     * functions allow for creating negative space within custom shapes that are
     * flat. `beginContour()` begins adding vertices to a negative space and
     * <a href="#/p5/endContour">endContour()</a> stops adding them.
     * `beginContour()` and <a href="#/p5/endContour">endContour()</a> must be
     * called between <a href="#/p5/beginShape">beginShape()</a> and
     * <a href="#/p5/endShape">endShape()</a>.
     *
     * Transformations such as <a href="#/p5/translate">translate()</a>,
     * <a href="#/p5/rotate">rotate()</a>, and <a href="#/p5/scale">scale()</a>
     * don't work between `beginContour()` and
     * <a href="#/p5/endContour">endContour()</a>. It's also not possible to use
     * other shapes, such as <a href="#/p5/ellipse">ellipse()</a> or
     * <a href="#/p5/rect">rect()</a>, between `beginContour()` and
     * <a href="#/p5/endContour">endContour()</a>.
     *
     * Note: The vertices that define a negative space must "wind" in the opposite
     * direction from the outer shape. First, draw vertices for the outer shape
     * clockwise order. Then, draw vertices for the negative space in
     * counter-clockwise order.
     *
     * @method beginContour
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Start drawing the shape.
     *   beginShape();
     *
     *   // Exterior vertices, clockwise winding.
     *   vertex(10, 10);
     *   vertex(90, 10);
     *   vertex(90, 90);
     *   vertex(10, 90);
     *
     *   // Interior vertices, counter-clockwise winding.
     *   beginContour();
     *   vertex(30, 30);
     *   vertex(30, 70);
     *   vertex(70, 70);
     *   vertex(70, 30);
     *   endContour(CLOSE);
     *
     *   // Stop drawing the shape.
     *   endShape(CLOSE);
     *
     *   describe('A white square with a square hole in its center drawn on a gray background.');
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white square with a square hole in its center drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Start drawing the shape.
     *   beginShape();
     *
     *   // Exterior vertices, clockwise winding.
     *   vertex(-40, -40);
     *   vertex(40, -40);
     *   vertex(40, 40);
     *   vertex(-40, 40);
     *
     *   // Interior vertices, counter-clockwise winding.
     *   beginContour();
     *   vertex(-20, -20);
     *   vertex(-20, 20);
     *   vertex(20, 20);
     *   vertex(20, -20);
     *   endContour(CLOSE);
     *
     *   // Stop drawing the shape.
     *   endShape(CLOSE);
     * }
     */
    fn.beginContour = function(kind) {
      this._renderer.beginContour(kind);
    };

    /**
     * Stops creating a hole within a flat shape.
     *
     * The <a href="#/p5/beginContour">beginContour()</a> and `endContour()`
     * functions allow for creating negative space within custom shapes that are
     * flat. <a href="#/p5/beginContour">beginContour()</a> begins adding vertices
     * to a negative space and `endContour()` stops adding them.
     * <a href="#/p5/beginContour">beginContour()</a> and `endContour()` must be
     * called between <a href="#/p5/beginShape">beginShape()</a> and
     * <a href="#/p5/endShape">endShape()</a>.
     *
     *  By default,
     * the controur has an `OPEN` end, and to close it,
     * call `endContour(CLOSE)`. The CLOSE contour mode closes splines smoothly.
     *
     * Transformations such as <a href="#/p5/translate">translate()</a>,
     * <a href="#/p5/rotate">rotate()</a>, and <a href="#/p5/scale">scale()</a>
     * don't work between <a href="#/p5/beginContour">beginContour()</a> and
     * `endContour()`. It's also not possible to use other shapes, such as
     * <a href="#/p5/ellipse">ellipse()</a> or <a href="#/p5/rect">rect()</a>,
     * between <a href="#/p5/beginContour">beginContour()</a> and `endContour()`.
     *
     * Note: The vertices that define a negative space must "wind" in the opposite
     * direction from the outer shape. First, draw vertices for the outer shape
     * clockwise order. Then, draw vertices for the negative space in
     * counter-clockwise order.
     *
     * @method endContour
     * @param {OPEN|CLOSE} [mode=OPEN] By default, the value is OPEN
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Start drawing the shape.
     *   beginShape();
     *
     *   // Exterior vertices, clockwise winding.
     *   vertex(10, 10);
     *   vertex(90, 10);
     *   vertex(90, 90);
     *   vertex(10, 90);
     *
     *   // Interior vertices, counter-clockwise winding.
     *   beginContour();
     *   vertex(30, 30);
     *   vertex(30, 70);
     *   vertex(70, 70);
     *   vertex(70, 30);
     *   endContour(CLOSE);
     *
     *   // Stop drawing the shape.
     *   endShape(CLOSE);
     *
     *   describe('A white square with a square hole in its center drawn on a gray background.');
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white square with a square hole in its center drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Start drawing the shape.
     *   beginShape();
     *
     *   // Exterior vertices, clockwise winding.
     *   vertex(-40, -40);
     *   vertex(40, -40);
     *   vertex(40, 40);
     *   vertex(-40, 40);
     *
     *   // Interior vertices, counter-clockwise winding.
     *   beginContour();
     *   vertex(-20, -20);
     *   vertex(-20, 20);
     *   vertex(20, 20);
     *   vertex(20, -20);
     *   endContour(CLOSE);
     *
     *   // Stop drawing the shape.
     *   endShape(CLOSE);
     * }
     */
    fn.endContour = function(mode = OPEN) {
      this._renderer.endContour(mode);
    };
  }

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

  class States {
    #modified = {};

    constructor(initialState) {
      for (const key in initialState) {
        this[key] = initialState[key];
      }
    }

    setValue(key, value) {
      if (!(key in this.#modified)) {
        this.#modified[key] = this[key];
      }
      this[key] = value;
    }

    getDiff() {
      const diff = this.#modified;
      this.#modified = {};
      return diff;
    }

    getModified() {
      return this.#modified;
    }

    applyDiff(prevModified) {
      for (const key in this.#modified) {
        this[key] = this.#modified[key];
      }
      this.#modified = prevModified;
    }
  }

  /**
   * @module Rendering
   * @submodule Rendering
   * @for p5
   */


  class ClonableObject {
    constructor(obj = {}) {
      for (const key in obj) {
        this[key] = obj[key];
      }
    }

    clone() {
      return new ClonableObject(this);
    }
  }
  class Renderer {
    static states = {
      strokeColor: null,
      strokeSet: false,
      fillColor: null,
      fillSet: false,
      tint: null,

      imageMode: CORNER,
      rectMode: CORNER,
      ellipseMode: CENTER,
      strokeWeight: 1,

      textFont: { family: 'sans-serif' },
      textLeading: 15,
      leadingSet: false,
      textSize: 12,
      textAlign: LEFT,
      textBaseline: BASELINE,
      bezierOrder: 3,
      splineProperties: new ClonableObject({
        ends: INCLUDE,
        tightness: 0
      }),
      textWrap: WORD,

      // added v2.0
      fontStyle: NORMAL, // v1: textStyle
      fontStretch: NORMAL,
      fontWeight: NORMAL,
      lineHeight: NORMAL,
      fontVariant: NORMAL,
      direction: 'inherit'
    };

    constructor(pInst, w, h, isMainCanvas) {
      this._pInst = pInst;
      this._isMainCanvas = isMainCanvas;
      this.pixels = [];
      this._pixelDensity = Math.ceil(window.devicePixelRatio) || 1;

      this.width = w;
      this.height = h;

      this._events = {};

      if (isMainCanvas) {
        this._isMainCanvas = true;
      }

      // Renderer state machine
      this.states = new States(Renderer.states);

      this.states.strokeColor = new Color([0, 0, 0]);
      this.states.fillColor = new Color([1, 1, 1]);

      this._pushPopStack = [];
      // NOTE: can use the length of the push pop stack instead
      this._pushPopDepth = 0;

      this._clipping = false;
      this._clipInvert = false;

      this._currentShape = undefined; // Lazily generate current shape
    }

    get currentShape() {
      if (!this._currentShape) {
        this._currentShape = new Shape(this.getCommonVertexProperties());
      }
      return this._currentShape;
    }

    remove() {

    }

    pixelDensity(val){
      let returnValue;
      if (typeof val === 'number') {
        if (val !== this._pixelDensity) {
          this._pixelDensity = val;
        }
        returnValue = this;
        this.resize(this.width, this.height);
      } else {
        returnValue = this._pixelDensity;
      }
      return returnValue;
    }

    // Makes a shallow copy of the current states
    // and push it into the push pop stack
    push() {
      this._pushPopDepth++;
      this._pushPopStack.push(this.states.getDiff());
    }

    // Pop the previous states out of the push pop stack and
    // assign it back to the current state
    pop() {
      this._pushPopDepth--;
      const diff = this._pushPopStack.pop() || {};
      const modified = this.states.getModified();
      this.states.applyDiff(diff);
      this.updateShapeVertexProperties(modified);
      this.updateShapeProperties(modified);
    }

    bezierOrder(order) {
      if (order === undefined) {
        return this.states.bezierOrder;
      } else {
        this.states.setValue('bezierOrder', order);
        this.updateShapeProperties();
      }
    }

    bezierVertex(x, y, z = 0, u = 0, v = 0) {
      const position = new Vector(x, y, z);
      const textureCoordinates = this.getSupportedIndividualVertexProperties()
        .textureCoordinates
        ? new Vector(u, v)
        : undefined;
      this.currentShape.bezierVertex(position, textureCoordinates);
    }

    splineProperty(key, value) {
      if (value === undefined) {
        return this.states.splineProperties[key];
      } else {
        this.states.setValue('splineProperties', this.states.splineProperties.clone());
        this.states.splineProperties[key] = value;
      }
      this.updateShapeProperties();
    }

    splineProperties(values) {
      if (values) {
        for (const key in values) {
          this.splineProperty(key, values[key]);
        }
      } else {
        return { ...this.states.splineProperties };
      }
    }

    splineVertex(x, y, z = 0, u = 0, v = 0) {
      const position = new Vector(x, y, z);
      const textureCoordinates = this.getSupportedIndividualVertexProperties()
        .textureCoordinates
        ? new Vector(u, v)
        : undefined;
      this.currentShape.splineVertex(position, textureCoordinates);
    }

    curveDetail(d) {
      if (d === undefined) {
        return this.states.curveDetail;
      } else {
        this.states.setValue('curveDetail', d);
      }
    }

    beginShape(...args) {
      this.currentShape.reset();
      this.updateShapeVertexProperties();
      this.currentShape.beginShape(...args);
    }

    endShape(...args) {
      this.currentShape.endShape(...args);
      this.drawShape(this.currentShape);
    }

    beginContour(shapeKind) {
      this.currentShape.beginContour(shapeKind);
    }

    endContour(mode) {
      this.currentShape.endContour(mode);
    }

    drawShape(shape, count) {
      throw new Error('Unimplemented');
    }

    vertex(x, y, z = 0, u = 0, v = 0) {
      const position = new Vector(x, y, z);
      const textureCoordinates = this.getSupportedIndividualVertexProperties()
        .textureCoordinates
        ? new Vector(u, v)
        : undefined;
      this.currentShape.vertex(position, textureCoordinates);
    }

    bezier(x1, y1, x2, y2, x3, y3, x4, y4) {
      const oldOrder = this._pInst.bezierOrder();
      this._pInst.bezierOrder(oldOrder);
      this._pInst.beginShape();
      this._pInst.bezierVertex(x1, y1);
      this._pInst.bezierVertex(x2, y2);
      this._pInst.bezierVertex(x3, y3);
      this._pInst.bezierVertex(x4, y4);
      this._pInst.endShape();
      return this;
    }

    spline(...args) {
      if (args.length === 2 * 4) {
        const [x1, y1, x2, y2, x3, y3, x4, y4] = args;
        this._pInst.beginShape();
        this._pInst.splineVertex(x1, y1);
        this._pInst.splineVertex(x2, y2);
        this._pInst.splineVertex(x3, y3);
        this._pInst.splineVertex(x4, y4);
        this._pInst.endShape();
      } else if (args.length === 3 * 4) {
        const [x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4] = args;
        this._pInst.beginShape();
        this._pInst.splineVertex(x1, y1, z1);
        this._pInst.splineVertex(x2, y2, z2);
        this._pInst.splineVertex(x3, y3, z3);
        this._pInst.splineVertex(x4, y4, z4);
        this._pInst.endShape();
      }
      return this;
    }

    beginClip(options = {}) {
      if (this._clipping) {
        throw new Error("It looks like you're trying to clip while already in the middle of clipping. Did you forget to endClip()?");
      }
      this._clipping = true;
      this._clipInvert = options.invert;
    }

    endClip() {
      if (!this._clipping) {
        throw new Error("It looks like you've called endClip() without beginClip(). Did you forget to call beginClip() first?");
      }
      this._clipping = false;
    }

    /**
     * Resize our canvas element.
     */
    resize(w, h) {
      this.width = w;
      this.height = h;
    }

    get(x, y, w, h) {
      const pd = this._pixelDensity;
      const canvas = this.canvas;

      if (typeof x === 'undefined' && typeof y === 'undefined') {
      // get()
        x = y = 0;
        w = this.width;
        h = this.height;
      } else {
        x *= pd;
        y *= pd;

        if (typeof w === 'undefined' && typeof h === 'undefined') {
        // get(x,y)
          if (x < 0 || y < 0 || x >= canvas.width || y >= canvas.height) {
            return [0, 0, 0, 0];
          }

          return this._getPixel(x, y);
        }
      // get(x,y,w,h)
      }

      const region = new Image(w*pd, h*pd);
      region.pixelDensity(pd);
      region.canvas
        .getContext('2d')
        .drawImage(canvas, x, y, w * pd, h * pd, 0, 0, w*pd, h*pd);

      return region;
    }

    scale(x, y){

    }

    fill(...args) {
      this.states.setValue('fillSet', true);
      this.states.setValue('fillColor', this._pInst.color(...args));
      this.updateShapeVertexProperties();
    }

    noFill() {
      this.states.setValue('fillColor', null);
    }

    strokeWeight(w) {
      if (w === undefined) {
        return this.states.strokeWeight;
      } else {
        this.states.setValue('strokeWeight', w);
      }
    }

    stroke(...args) {
      this.states.setValue('strokeSet', true);
      this.states.setValue('strokeColor', this._pInst.color(...args));
      this.updateShapeVertexProperties();
    }

    noStroke() {
      this.states.setValue('strokeColor', null);
    }

    getCommonVertexProperties() {
      return {};
    }

    getSupportedIndividualVertexProperties() {
      return {
        textureCoordinates: false
      };
    }

    updateShapeProperties(modified) {
      if (!modified || modified.bezierOrder || modified.splineProperties) {
        const shape = this.currentShape;
        shape.bezierOrder(this.states.bezierOrder);
        shape.splineProperty('ends', this.states.splineProperties.ends);
        shape.splineProperty('tightness', this.states.splineProperties.tightness);
      }
    }

    updateShapeVertexProperties(modified) {
      const props = this.getCommonVertexProperties();
      if (!modified || Object.keys(modified).some(k => k in props)) {
        const shape = this.currentShape;
        for (const key in props) {
          shape[key](props[key]);
        }
      }
    }

    _applyDefaults() {
      return this;
    }

    finishDraw() {
      // Default no-op implementation
      // Override in specific renderers as needed
    }

    ///////////////////////////////
    //// TEXT SUPPORT METHODS
    //////////////////////////////

    _middleAlignOffset = function() {
      const { textFont, textSize } = this.states;
      const font = textFont?.font;
      const ctx = this.textDrawingContext();
      const metrics = ctx.measureText('X');
      let sCapHeight = (font?.data || {})['OS/2']?.sCapHeight;
      if (sCapHeight) {
        const unitsPerEm = font.data.head.unitsPerEm;
        sCapHeight *= textSize / unitsPerEm;
      } else {
        sCapHeight = metrics.fontBoundingBoxAscent;
      }
      return metrics.alphabeticBaseline + sCapHeight / 2;
    };
  }
  function renderer(p5, fn){
    /**
     * Main graphics and rendering context, as well as the base API
     * implementation for p5.js "core". To be used as the superclass for
     * Renderer2D and Renderer3D classes, respectively.
     *
     * @class p5.Renderer
     * @param {HTMLElement} elt DOM node that is wrapped
     * @param {p5} [pInst] pointer to p5 instance
     * @param {Boolean} [isMainCanvas] whether we're using it as main canvas
     * @private
     */
    p5.Renderer = Renderer;
  }

  /**
   * @module DOM
   * @submodule DOM
   */


  /**
   * @typedef {'video'} VIDEO
   * @property {VIDEO} VIDEO
   * @final
   */
  const VIDEO = 'video';

  /**
   * @typedef {'audio'} AUDIO
   * @property {AUDIO} AUDIO
   * @final
   */
  const AUDIO = 'audio';

  class Cue {
    constructor(callback, time, id, val) {
      this.callback = callback;
      this.time = time;
      this.id = id;
      this.val = val;
    }
  }

  class MediaElement extends Element {
    constructor(elt, pInst) {
      super(elt, pInst);

      const self = this;
      this.elt.crossOrigin = 'anonymous';

      this._prevTime = 0;
      this._cueIDCounter = 0;
      this._cues = [];
      this.pixels = [];
      this._pixelsState = this;
      this._pixelDensity = 1;
      this._modified = false;

      // Media has an internal canvas that is used when drawing it to the main
      // canvas. It will need to be updated each frame as the video itself plays.
      // We don't want to update it every time we draw, however, in case the user
      // has used load/updatePixels. To handle this, we record the frame drawn to
      // the internal canvas so we only update it if the frame has changed.
      this._frameOnCanvas = -1;

      Object.defineProperty(self, 'src', {
        get() {
          const firstChildSrc = self.elt.children[0].src;
          const srcVal = self.elt.src === window.location.href ? '' : self.elt.src;
          const ret =
            firstChildSrc === window.location.href ? srcVal : firstChildSrc;
          return ret;
        },
        set(newValue) {
          for (let i = 0; i < self.elt.children.length; i++) {
            self.elt.removeChild(self.elt.children[i]);
          }
          const source = document.createElement('source');
          source.src = newValue;
          elt.appendChild(source);
          self.elt.src = newValue;
          self._modified = true;
        }
      });

      // private _onended callback, set by the method: onended(callback)
      self._onended = function () { };
      self.elt.onended = function () {
        self._onended(self);
      };
    }


    /**
     * Plays audio or video from a media element.
     *
     * @chainable
     *
     * @example
     * let beat;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display a message.
     *   text('Click to play', 50, 50);
     *
     *   // Create a p5.MediaElement using createAudio().
     *   beat = createAudio('assets/beat.mp3');
     *
     *   describe('The text "Click to play" written in black on a gray background. A beat plays when the user clicks the square.');
     * }
     *
     * // Play the beat when the user presses the mouse.
     * function mousePressed() {
     *   beat.play();
     * }
     */
    play() {
      if (this.elt.currentTime === this.elt.duration) {
        this.elt.currentTime = 0;
      }
      let promise;
      if (this.elt.readyState > 1) {
        promise = this.elt.play();
      } else {
        // in Chrome, playback cannot resume after being stopped and must reload
        this.elt.load();
        promise = this.elt.play();
      }
      if (promise && promise.catch) {
        promise.catch(e => {
          // if it's an autoplay failure error
          if (e.name === 'NotAllowedError') {
            if (typeof IS_MINIFIED === 'undefined') {
              p5._friendlyAutoplayError(this.src);
            } else {
              console.error(e);
            }
          } else {
            // any other kind of error
            console.error('Media play method encountered an unexpected error', e);
          }
        });
      }
      return this;
    }

    /**
     * Stops a media element and sets its current time to 0.
     *
     * Calling `media.play()` will restart playing audio/video from the beginning.
     *
     * @chainable
     *
     * @example
     * let beat;
     * let isStopped = true;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.MediaElement using createAudio().
     *   beat = createAudio('assets/beat.mp3');
     *
     *   describe('The text "Click to start" written in black on a gray background. The beat starts or stops when the user presses the mouse.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display different instructions based on playback.
     *   if (isStopped === true) {
     *     text('Click to start', 50, 50);
     *   } else {
     *     text('Click to stop', 50, 50);
     *   }
     * }
     *
     * // Adjust playback when the user presses the mouse.
     * function mousePressed() {
     *   if (isStopped === true) {
     *     // If the beat is stopped, play it.
     *     beat.play();
     *     isStopped = false;
     *   } else {
     *     // If the beat is playing, stop it.
     *     beat.stop();
     *     isStopped = true;
     *   }
     * }
     */
    stop() {
      this.elt.pause();
      this.elt.currentTime = 0;
      return this;
    }

    /**
     * Pauses a media element.
     *
     * Calling `media.play()` will resume playing audio/video from the moment it paused.
     *
     * @chainable
     *
     * @example
     * let beat;
     * let isPaused = true;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.MediaElement using createAudio().
     *   beat = createAudio('assets/beat.mp3');
     *
     *   describe('The text "Click to play" written in black on a gray background. The beat plays or pauses when the user clicks the square.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display different instructions based on playback.
     *   if (isPaused === true) {
     *     text('Click to play', 50, 50);
     *   } else {
     *     text('Click to pause', 50, 50);
     *   }
     * }
     *
     * // Adjust playback when the user presses the mouse.
     * function mousePressed() {
     *   if (isPaused === true) {
     *     // If the beat is paused,
     *     // play it.
     *     beat.play();
     *     isPaused = false;
     *   } else {
     *     // If the beat is playing,
     *     // pause it.
     *     beat.pause();
     *     isPaused = true;
     *   }
     * }
     */
    pause() {
      this.elt.pause();
      return this;
    }

    /**
     * Plays the audio/video repeatedly in a loop.
     *
     * @chainable
     *
     * @example
     * let beat;
     * let isLooping = false;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.MediaElement using createAudio().
     *   beat = createAudio('assets/beat.mp3');
     *
     *   describe('The text "Click to loop" written in black on a gray background. A beat plays repeatedly in a loop when the user clicks. The beat stops when the user clicks again.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display different instructions based on playback.
     *   if (isLooping === true) {
     *     text('Click to stop', 50, 50);
     *   } else {
     *     text('Click to loop', 50, 50);
     *   }
     * }
     *
     * // Adjust playback when the user presses the mouse.
     * function mousePressed() {
     *   if (isLooping === true) {
     *     // If the beat is looping, stop it.
     *     beat.stop();
     *     isLooping = false;
     *   } else {
     *     // If the beat is stopped, loop it.
     *     beat.loop();
     *     isLooping = true;
     *   }
     * }
     */
    loop() {
      this.elt.setAttribute('loop', true);
      this.play();
      return this;
    }
    /**
     * Stops the audio/video from playing in a loop.
     *
     * The media will stop when it finishes playing.
     *
     * @chainable
     *
     * @example
     * let beat;
     * let isPlaying = false;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.MediaElement using createAudio().
     *   beat = createAudio('assets/beat.mp3');
     *
     *   describe('The text "Click to play" written in black on a gray background. A beat plays when the user clicks. The beat stops when the user clicks again.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display different instructions based on playback.
     *   if (isPlaying === true) {
     *     text('Click to stop', 50, 50);
     *   } else {
     *     text('Click to play', 50, 50);
     *   }
     * }
     *
     * // Adjust playback when the user presses the mouse.
     * function mousePressed() {
     *   if (isPlaying === true) {
     *     // If the beat is playing, stop it.
     *     beat.stop();
     *     isPlaying = false;
     *   } else {
     *     // If the beat is stopped, play it.
     *     beat.play();
     *     isPlaying = true;
     *   }
     * }
     */
    noLoop() {
      this.elt.removeAttribute('loop');
      return this;
    }

    /**
     * Sets up logic to check that autoplay succeeded.
     *
     * @private
     */
    _setupAutoplayFailDetection() {
      const timeout = setTimeout(() => {
        if (typeof IS_MINIFIED === 'undefined') {
          p5._friendlyAutoplayError(this.src);
        } else {
          console.error(e);
        }
      }, 500);
      this.elt.addEventListener('play', () => clearTimeout(timeout), {
        passive: true,
        once: true
      });
    }

    /**
     * Sets the audio/video to play once it's loaded.
     *
     * The parameter, `shouldAutoplay`, is optional. Calling
     * `media.autoplay()` without an argument causes the media to play
     * automatically. If `true` is passed, as in `media.autoplay(true)`, the
     * media will automatically play. If `false` is passed, as in
     * `media.autoPlay(false)`, it won't play automatically.
     *
     * @param {Boolean} [shouldAutoplay] whether the element should autoplay.
     * @chainable
     *
     * @example
     * let video;
     *
     * function setup() {
     *   noCanvas();
     *
     *   // Call handleVideo() once the video loads.
     *   video = createVideo('assets/fingers.mov', handleVideo);
     *
     *   describe('A video of fingers walking on a treadmill.');
     * }
     *
     * // Set the video's size and play it.
     * function handleVideo() {
     *   video.size(100, 100);
     *   video.autoplay();
     * }
     *
     * @example
     * function setup() {
     *   noCanvas();
     *
     *   // Load a video, but don't play it automatically.
     *   let video = createVideo('assets/fingers.mov', handleVideo);
     *
     *   // Play the video when the user clicks on it.
     *   video.mousePressed(handlePress);
     *
     *   describe('An image of fingers on a treadmill. They start walking when the user double-clicks on them.');
     * }
     *
     * // Set the video's size and playback mode.
     * function handleVideo() {
     *   video.size(100, 100);
     *   video.autoplay(false);
     * }
     *
     * // Play the video.
     * function handleClick() {
     *   video.play();
     * }
     */
    autoplay(val) {
      const oldVal = this.elt.getAttribute('autoplay');
      this.elt.setAttribute('autoplay', val);
      // if we turned on autoplay
      if (val && !oldVal) {
        // bind method to this scope
        const setupAutoplayFailDetection =
          () => this._setupAutoplayFailDetection();
        // if media is ready to play, schedule check now
        if (this.elt.readyState === 4) {
          setupAutoplayFailDetection();
        } else {
          // otherwise, schedule check whenever it is ready
          this.elt.addEventListener('canplay', setupAutoplayFailDetection, {
            passive: true,
            once: true
          });
        }
      }

      return this;
    }

    /**
     * Sets the audio/video volume.
     *
     * Calling `media.volume()` without an argument returns the current volume
     * as a number in the range 0 (off) to 1 (maximum).
     *
     * The parameter, `val`, is optional. It's a number that sets the volume
     * from 0 (off) to 1 (maximum). For example, calling `media.volume(0.5)`
     * sets the volume to half of its maximum.
     *
     * @return {Number} current volume.
     *
     * @example
     * let dragon;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.MediaElement using createAudio().
     *   dragon = createAudio('assets/lucky_dragons.mp3');
     *
     *   // Show the default media controls.
     *   dragon.showControls();
     *
     *   describe('The text "Volume: V" on a gray square with media controls beneath it. The number "V" oscillates between 0 and 1 as the music plays.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Produce a number between 0 and 1.
     *   let n = 0.5 * sin(frameCount * 0.01) + 0.5;
     *
     *   // Use n to set the volume.
     *   dragon.volume(n);
     *
     *   // Get the current volume and display it.
     *   let v = dragon.volume();
     *
     *   // Round v to 1 decimal place for display.
     *   v = round(v, 1);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the volume.
     *   text(`Volume: ${v}`, 50, 50);
     * }
     */
    /**
     * @param {Number}            val volume between 0.0 and 1.0.
     * @chainable
     */
    volume(val) {
      if (typeof val === 'undefined') {
        return this.elt.volume;
      } else {
        this.elt.volume = val;
      }
    }

    /**
     * Sets the audio/video playback speed.
     *
     * The parameter, `val`, is optional. It's a number that sets the playback
     * speed. 1 plays the media at normal speed, 0.5 plays it at half speed, 2
     * plays it at double speed, and so on. -1 plays the media at normal speed
     * in reverse.
     *
     * Calling `media.speed()` returns the current speed as a number.
     *
     * Note: Not all browsers support backward playback. Even if they do,
     * playback might not be smooth.
     *
     * @return {Number} current playback speed.
     *
     * @example
     * let dragon;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.MediaElement using createAudio().
     *   dragon = createAudio('assets/lucky_dragons.mp3');
     *
     *   // Show the default media controls.
     *   dragon.showControls();
     *
     *   describe('The text "Speed: S" on a gray square with media controls beneath it. The number "S" oscillates between 0 and 1 as the music plays.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Produce a number between 0 and 2.
     *   let n = sin(frameCount * 0.01) + 1;
     *
     *   // Use n to set the playback speed.
     *   dragon.speed(n);
     *
     *   // Get the current speed and display it.
     *   let s = dragon.speed();
     *
     *   // Round s to 1 decimal place for display.
     *   s = round(s, 1);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the speed.
     *   text(`Speed: ${s}`, 50, 50);
     * }
     */
    /**
     * @param {Number} speed  speed multiplier for playback.
     * @chainable
     */
    speed(val) {
      if (typeof val === 'undefined') {
        return this.presetPlaybackRate || this.elt.playbackRate;
      } else {
        if (this.loadedmetadata) {
          this.elt.playbackRate = val;
        } else {
          this.presetPlaybackRate = val;
        }
      }
    }

    /**
     * Sets the media element's playback time.
     *
     * The parameter, `time`, is optional. It's a number that specifies the
     * time, in seconds, to jump to when playback begins.
     *
     * Calling `media.time()` without an argument returns the number of seconds
     * the audio/video has played.
     *
     * Note: Time resets to 0 when looping media restarts.
     *
     * @param {Number} [time] time to jump to (in seconds).
     * @return {Number} current time (in seconds).
     *
     * @example
     * let dragon;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.MediaElement using createAudio().
     *   dragon = createAudio('assets/lucky_dragons.mp3');
     *
     *   // Show the default media controls.
     *   dragon.showControls();
     *
     *   describe('The text "S seconds" on a gray square with media controls beneath it. The number "S" increases as the song plays.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Get the current playback time.
     *   let s = dragon.time();
     *
     *   // Round s to 1 decimal place for display.
     *   s = round(s, 1);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the playback time.
     *   text(`${s} seconds`, 50, 50);
     * }
     *
     * @example
     * let dragon;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.MediaElement using createAudio().
     *   dragon = createAudio('assets/lucky_dragons.mp3');
     *
     *   // Show the default media controls.
     *   dragon.showControls();
     *
     *   // Jump to 2 seconds to start.
     *   dragon.time(2);
     *
     *   describe('The text "S seconds" on a gray square with media controls beneath it. The number "S" increases as the song plays.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Get the current playback time.
     *   let s = dragon.time();
     *
     *   // Round s to 1 decimal place for display.
     *   s = round(s, 1);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the playback time.
     *   text(`${s} seconds`, 50, 50);
     * }
     */
    time(val) {
      if (typeof val !== 'undefined') {
        this.elt.currentTime = val;
      }
      return this.elt.currentTime;
    }

    /**
     * Returns the audio/video's duration in seconds.
     *
     * @return {Number} duration (in seconds).
     *
     * @example
     * let dragon;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.MediaElement using createAudio().
     *   dragon = createAudio('assets/lucky_dragons.mp3');
     *
     *   // Show the default media controls.
     *   dragon.showControls();
     *
     *   describe('The text "S seconds left" on a gray square with media controls beneath it. The number "S" decreases as the song plays.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the time remaining.
     *   let s = dragon.duration() - dragon.time();
     *
     *   // Round s to 1 decimal place for display.
     *   s = round(s, 1);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the time remaining.
     *   text(`${s} seconds left`, 50, 50);
     * }
     */
    duration() {
      return this.elt.duration;
    }
    _ensureCanvas() {
      if (!this.canvas) {
        this.canvas = document.createElement('canvas');
        this.drawingContext = this.canvas.getContext('2d');
        this.setModified(true);
      }

      // Don't update the canvas again if we have already updated the canvas with
      // the current frame
      const needsRedraw = this._frameOnCanvas !== this._pInst.frameCount;
      if (this.loadedmetadata && needsRedraw) {
        // wait for metadata for w/h
        if (this.canvas.width !== this.elt.width) {
          this.canvas.width = this.elt.width;
          this.canvas.height = this.elt.height;
          this.width = this.canvas.width;
          this.height = this.canvas.height;
        }

        this.drawingContext.clearRect(
          0, 0, this.canvas.width, this.canvas.height);

        if (this.flipped === true) {
          this.drawingContext.save();
          this.drawingContext.scale(-1, 1);
          this.drawingContext.translate(-this.canvas.width, 0);
        }

        this.drawingContext.drawImage(
          this.elt,
          0,
          0,
          this.canvas.width,
          this.canvas.height
        );

        if (this.flipped === true) {
          this.drawingContext.restore();
        }

        this.setModified(true);
        this._frameOnCanvas = this._pInst.frameCount;
      }
    }
    loadPixels(...args) {
      this._ensureCanvas();
      return p5.Renderer2D.prototype.loadPixels.apply(this, args);
    }
    updatePixels(x, y, w, h) {
      if (this.loadedmetadata) {
        // wait for metadata
        this._ensureCanvas();
        p5.Renderer2D.prototype.updatePixels.call(this, x, y, w, h);
      }
      this.setModified(true);
      return this;
    }
    get(...args) {
      this._ensureCanvas();
      return p5.Renderer2D.prototype.get.apply(this, args);
    }
    _getPixel(...args) {
      this.loadPixels();
      return p5.Renderer2D.prototype._getPixel.apply(this, args);
    }

    set(x, y, imgOrCol) {
      if (this.loadedmetadata) {
        // wait for metadata
        this._ensureCanvas();
        p5.Renderer2D.prototype.set.call(this, x, y, imgOrCol);
        this.setModified(true);
      }
    }
    copy(...args) {
      this._ensureCanvas();
      p5.prototype.copy.apply(this, args);
    }
    mask(...args) {
      this.loadPixels();
      this.setModified(true);
      p5.Image.prototype.mask.apply(this, args);
    }
    /**
     * helper method for web GL mode to figure out if the element
     * has been modified and might need to be re-uploaded to texture
     * memory between frames.
     * @private
     * @return {boolean} a boolean indicating whether or not the
     * image has been updated or modified since last texture upload.
     */
    isModified() {
      return this._modified;
    }
    /**
     * helper method for web GL mode to indicate that an element has been
     * changed or unchanged since last upload. gl texture upload will
     * set this value to false after uploading the texture; or might set
     * it to true if metadata has become available but there is no actual
     * texture data available yet..
     * @param {Boolean} val sets whether or not the element has been
     * modified.
     * @private
     */
    setModified(value) {
      this._modified = value;
    }
    /**
     * Calls a function when the audio/video reaches the end of its playback.
     *
     * The element is passed as an argument to the callback function.
     *
     * Note: The function won't be called if the media is looping.
     *
     * @param  {Function} callback function to call when playback ends.
     *                             The `p5.MediaElement` is passed as
     *                             the argument.
     * @chainable
     *
     * @example
     * let beat;
     * let isPlaying = false;
     * let isDone = false;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.MediaElement using createAudio().
     *   beat = createAudio('assets/beat.mp3');
     *
     *   // Call handleEnd() when the beat finishes.
     *   beat.onended(handleEnd);
     *
     *   describe('The text "Click to play" written in black on a gray square. A beat plays when the user clicks. The text "Done!" appears when the beat finishes playing.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display different messages based on playback.
     *   if (isDone === true) {
     *     text('Done!', 50, 50);
     *   } else if (isPlaying === false) {
     *     text('Click to play', 50, 50);
     *   } else {
     *     text('Playing...', 50, 50);
     *   }
     * }
     *
     * // Play the beat when the user presses the mouse.
     * function mousePressed() {
     *   if (isPlaying === false) {
     *     isPlaying = true;
     *     beat.play();
     *   }
     * }
     *
     * // Set isDone when playback ends.
     * function handleEnd() {
     *   isDone = false;
     * }
     */
    onended(callback) {
      this._onended = callback;
      return this;
    }

    /*** CONNECT TO WEB AUDIO API / p5.sound.js ***/

    _getAudioContext() {
      return undefined;
    }
    _getSoundOut() {
      return undefined;
    }

    /**
     * Sends the element's audio to an output.
     *
     * The parameter, `audioNode`, can be an `AudioNode` or an object from the
     * `p5.sound` library.
     *
     * If no element is provided, as in `myElement.connect()`, the element
     * connects to the main output. All connections are removed by the
     * `.disconnect()` method.
     *
     * Note: This method is meant to be used with the p5.sound.js addon library.
     *
     * @param  {AudioNode|Object} audioNode AudioNode from the Web Audio API,
     * or an object from the p5.sound library
     */
    connect(obj) {
      let audioContext, mainOutput;

      // if p5.sound exists, same audio context
      if (this._getAudioContext() && this._getSoundOut()) {
        audioContext = this._getAudioContext();
        mainOutput = this._getSoundOut().input;
      } else {
        try {
          audioContext = obj.context;
          mainOutput = audioContext.destination;
        } catch (e) {
          throw 'connect() is meant to be used with Web Audio API or p5.sound.js';
        }
      }

      // create a Web Audio MediaElementAudioSourceNode if none already exists
      if (!this.audioSourceNode) {
        this.audioSourceNode = audioContext.createMediaElementSource(this.elt);

        // connect to main output when this method is first called
        this.audioSourceNode.connect(mainOutput);
      }

      // connect to object if provided
      if (obj) {
        if (obj.input) {
          this.audioSourceNode.connect(obj.input);
        } else {
          this.audioSourceNode.connect(obj);
        }
      } else {
        // otherwise connect to main output of p5.sound / AudioContext
        this.audioSourceNode.connect(mainOutput);
      }
    }

    /**
     * Disconnect all Web Audio routing, including to the main output.
     *
     * This is useful if you want to re-route the output through audio effects,
     * for example.
     *
     */
    disconnect() {
      if (this.audioSourceNode) {
        this.audioSourceNode.disconnect();
      } else {
        throw 'nothing to disconnect';
      }
    }

    /*** SHOW / HIDE CONTROLS ***/

    /**
     * Show the default
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement" target="_blank">HTMLMediaElement</a>
     * controls.
     *
     * Note: The controls vary between web browsers.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background('cornflowerblue');
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(50);
     *
     *   // Display a dragon.
     *   text('🐉', 50, 50);
     *
     *   // Create a p5.MediaElement using createAudio().
     *   let dragon = createAudio('assets/lucky_dragons.mp3');
     *
     *   // Show the default media controls.
     *   dragon.showControls();
     *
     *   describe('A dragon emoji, 🐉, drawn in the center of a blue square. A song plays in the background. Audio controls are displayed beneath the canvas.');
     * }
     */
    showControls() {
      // must set style for the element to show on the page
      this.elt.style['text-align'] = 'inherit';
      this.elt.controls = true;
    }

    /**
     * Hide the default
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement" target="_blank">HTMLMediaElement</a>
     * controls.
     *
     * @example
     * let dragon;
     * let isHidden = false;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.MediaElement using createAudio().
     *   dragon = createAudio('assets/lucky_dragons.mp3');
     *
     *   // Show the default media controls.
     *   dragon.showControls();
     *
     *   describe('The text "Double-click to hide controls" written in the middle of a gray square. A song plays in the background. Audio controls are displayed beneath the canvas. The controls appear/disappear when the user double-clicks the square.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *
     *   // Display a different message when controls are hidden or shown.
     *   if (isHidden === true) {
     *     text('Double-click to show controls', 10, 20, 80, 80);
     *   } else {
     *     text('Double-click to hide controls', 10, 20, 80, 80);
     *   }
     * }
     *
     * // Show/hide controls based on a double-click.
     * function doubleClicked() {
     *   if (isHidden === true) {
     *     dragon.showControls();
     *     isHidden = false;
     *   } else {
     *     dragon.hideControls();
     *     isHidden = true;
     *   }
     * }
     */
    hideControls() {
      this.elt.controls = false;
    }

    /**
     * Schedules a function to call when the audio/video reaches a specific time
     * during its playback.
     *
     * The first parameter, `time`, is the time, in seconds, when the function
     * should run. This value is passed to `callback` as its first argument.
     *
     * The second parameter, `callback`, is the function to call at the specified
     * cue time.
     *
     * The third parameter, `value`, is optional and can be any type of value.
     * `value` is passed to `callback`.
     *
     * Calling `media.addCue()` returns an ID as a string. This is useful for
     * removing the cue later.
     *
     * @param {Number}   time     cue time to run the callback function.
     * @param {Function} callback function to call at the cue time.
     * @param {Object} [value]    object to pass as the argument to
     *                            `callback`.
     * @return {Number} id ID of this cue,
     *                     useful for `media.removeCue(id)`.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.MediaElement using createAudio().
     *   let beat = createAudio('assets/beat.mp3');
     *
     *   // Play the beat in a loop.
     *   beat.loop();
     *
     *   // Schedule a few events.
     *   beat.addCue(0, changeBackground, 'red');
     *   beat.addCue(2, changeBackground, 'deeppink');
     *   beat.addCue(4, changeBackground, 'orchid');
     *   beat.addCue(6, changeBackground, 'lavender');
     *
     *   describe('A red square with a beat playing in the background. Its color changes every 2 seconds while the audio plays.');
     * }
     *
     * // Change the background color.
     * function changeBackground(c) {
     *   background(c);
     * }
     */
    addCue(time, callback, val) {
      const id = this._cueIDCounter++;

      const cue = new Cue(callback, time, id, val);
      this._cues.push(cue);

      if (!this.elt.ontimeupdate) {
        this.elt.ontimeupdate = this._onTimeUpdate.bind(this);
      }

      return id;
    }

    /**
     * Removes a callback based on its ID.
     *
     * @param  {Number} id ID of the cue, created by `media.addCue()`.
     *
     * @example
     * let lavenderID;
     * let isRemoved = false;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.MediaElement using createAudio().
     *   let beat = createAudio('assets/beat.mp3');
     *
     *   // Play the beat in a loop.
     *   beat.loop();
     *
     *   // Schedule a few events.
     *   beat.addCue(0, changeBackground, 'red');
     *   beat.addCue(2, changeBackground, 'deeppink');
     *   beat.addCue(4, changeBackground, 'orchid');
     *
     *   // Record the ID of the "lavender" callback.
     *   lavenderID = beat.addCue(6, changeBackground, 'lavender');
     *
     *   describe('The text "Double-click to remove lavender." written on a red square. The color changes every 2 seconds while the audio plays. The lavender option is removed when the user double-clicks the square.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Display different instructions based on the available callbacks.
     *   if (isRemoved === false) {
     *     text('Double-click to remove lavender.', 10, 10, 80, 80);
     *   } else {
     *     text('No more lavender.', 10, 10, 80, 80);
     *   }
     * }
     *
     * // Change the background color.
     * function changeBackground(c) {
     *   background(c);
     * }
     *
     * // Remove the lavender color-change cue when the user double-clicks.
     * function doubleClicked() {
     *   if (isRemoved === false) {
     *     beat.removeCue(lavenderID);
     *     isRemoved = true;
     *   }
     * }
     */
    removeCue(id) {
      for (let i = 0; i < this._cues.length; i++) {
        if (this._cues[i].id === id) {
          console.log(id);
          this._cues.splice(i, 1);
        }
      }

      if (this._cues.length === 0) {
        this.elt.ontimeupdate = null;
      }
    }

    /**
     * Removes all functions scheduled with `media.addCue()`.
     *
     * @example
     * let isChanging = true;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.MediaElement using createAudio().
     *   let beat = createAudio('assets/beat.mp3');
     *
     *   // Play the beat in a loop.
     *   beat.loop();
     *
     *   // Schedule a few events.
     *   beat.addCue(0, changeBackground, 'red');
     *   beat.addCue(2, changeBackground, 'deeppink');
     *   beat.addCue(4, changeBackground, 'orchid');
     *   beat.addCue(6, changeBackground, 'lavender');
     *
     *   describe('The text "Double-click to stop changing." written on a square. The color changes every 2 seconds while the audio plays. The color stops changing when the user double-clicks the square.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Display different instructions based on the available callbacks.
     *   if (isChanging === true) {
     *     text('Double-click to stop changing.', 10, 10, 80, 80);
     *   } else {
     *     text('No more changes.', 10, 10, 80, 80);
     *   }
     * }
     *
     * // Change the background color.
     * function changeBackground(c) {
     *   background(c);
     * }
     *
     * // Remove cued functions and stop changing colors when the user
     * // double-clicks.
     * function doubleClicked() {
     *   if (isChanging === true) {
     *     beat.clearCues();
     *     isChanging = false;
     *   }
     * }
     */
    clearCues() {
      this._cues = [];
      this.elt.ontimeupdate = null;
    }

    // private method that checks for cues to be fired if events
    // have been scheduled using addCue(callback, time).
    _onTimeUpdate() {
      const playbackTime = this.time();

      for (let i = 0; i < this._cues.length; i++) {
        const callbackTime = this._cues[i].time;
        const val = this._cues[i].val;

        if (this._prevTime < callbackTime && callbackTime <= playbackTime) {
          // pass the scheduled callbackTime as parameter to the callback
          this._cues[i].callback(val);
        }
      }

      this._prevTime = playbackTime;
    }
  }

  // Cue inspired by JavaScript setTimeout, and the
  // Tone.js Transport Timeline Event, MIT License Yotam Mann 2015 tonejs.org

  function media(p5, fn){
    /**
     * Helpers for create methods.
     */
    function addElement(elt, pInst, media) {
      const node = pInst._userNode ? pInst._userNode : document.body;
      node.appendChild(elt);
      const c = media
        ? new MediaElement(elt, pInst)
        : new Element(elt, pInst);
      pInst._elements.push(c);
      return c;
    }

    /* VIDEO STUFF */

    // Helps perform similar tasks for media element methods.
    function createMedia(pInst, type, src, callback) {
      const elt = document.createElement(type);

      // Create source elements from given sources
      src = src || '';
      if (typeof src === 'string') {
        src = [src];
      }
      for (const mediaSource of src) {
        const sourceEl = document.createElement('source');
        sourceEl.setAttribute('src', mediaSource);
        elt.appendChild(sourceEl);
      }

      const mediaEl = addElement(elt, pInst, true);
      mediaEl.loadedmetadata = false;

      // set width and height onload metadata
      elt.addEventListener('loadedmetadata', () => {
        mediaEl.width = elt.videoWidth;
        mediaEl.height = elt.videoHeight;

        // set elt width and height if not set
        if (mediaEl.elt.width === 0) mediaEl.elt.width = elt.videoWidth;
        if (mediaEl.elt.height === 0) mediaEl.elt.height = elt.videoHeight;
        if (mediaEl.presetPlaybackRate) {
          mediaEl.elt.playbackRate = mediaEl.presetPlaybackRate;
          delete mediaEl.presetPlaybackRate;
        }
        mediaEl.loadedmetadata = true;
      });

      // If callback is provided, attach to element
      if (typeof callback === 'function') {
        const callbackHandler = () => {
          callback(mediaEl);
          elt.removeEventListener('canplaythrough', callbackHandler);
        };
        elt.addEventListener('canplaythrough', callbackHandler);
      }

      return mediaEl;
    }

    /**
     * Creates a `&lt;video&gt;` element for simple audio/video playback.
     *
     * `createVideo()` returns a new
     * <a href="#/p5.MediaElement">p5.MediaElement</a> object. Videos are shown by
     * default. They can be hidden by calling `video.hide()` and drawn to the
     * canvas using <a href="#/p5/image">image()</a>.
     *
     * The first parameter, `src`, is the path the video. If a single string is
     * passed, as in `'assets/topsecret.mp4'`, a single video is loaded. An array
     * of strings can be used to load the same video in different formats. For
     * example, `['assets/topsecret.mp4', 'assets/topsecret.ogv', 'assets/topsecret.webm']`.
     * This is useful for ensuring that the video can play across different browsers with
     * different capabilities. See
     * <a href='https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats'>MDN</a>
     * for more information about supported formats.
     *
     * The second parameter, `callback`, is optional. It's a function to call once
     * the video is ready to play.
     *
     * @method createVideo
     * @param  {String|String[]} [src] path to a video file, or an array of paths for
     *                               supporting different browsers.
     * @param  {Function} [callback] function to call once the video is ready to play.
     * @return {p5.MediaElement}   new <a href="#/p5.MediaElement">p5.MediaElement</a> object.
     *
     * @example
     * function setup() {
     *   noCanvas();
     *
     *   // Load a video and add it to the page.
     *   // Note: this may not work in some browsers.
     *   let video = createVideo('assets/small.mp4');
     *
     *   // Show the default video controls.
     *   video.showControls();
     *
     *   describe('A video of a toy robot with playback controls beneath it.');
     * }
     *
     * @example
     * function setup() {
     *   noCanvas();
     *
     *   // Load a video and add it to the page.
     *   // Provide an array options for different file formats.
     *   let video = createVideo(
     *     ['assets/small.mp4', 'assets/small.ogv', 'assets/small.webm']
     *   );
     *
     *   // Show the default video controls.
     *   video.showControls();
     *
     *   describe('A video of a toy robot with playback controls beneath it.');
     * }
     *
     * @example
     * let video;
     *
     * function setup() {
     *   noCanvas();
     *
     *   // Load a video and add it to the page.
     *   // Provide an array options for different file formats.
     *   // Call mute() once the video loads.
     *   video = createVideo(
     *     ['assets/small.mp4', 'assets/small.ogv', 'assets/small.webm'],
     *     muteVideo
     *   );
     *
     *   // Show the default video controls.
     *   video.showControls();
     *
     *   describe('A video of a toy robot with playback controls beneath it.');
     * }
     *
     * // Mute the video once it loads.
     * function muteVideo() {
     *   video.volume(0);
     * }
     */
    fn.createVideo = function (src, callback) {
      // p5._validateParameters('createVideo', arguments);
      return createMedia(this, VIDEO, src, callback);
    };

    /* AUDIO STUFF */

    /**
     * Creates a hidden `&lt;audio&gt;` element for simple audio playback.
     *
     * `createAudio()` returns a new
     * <a href="#/p5.MediaElement">p5.MediaElement</a> object.
     *
     * The first parameter, `src`, is the path the audio. If a single string is
     * passed, as in `'assets/audio.mp3'`, a single audio is loaded. An array
     * of strings can be used to load the same audio in different formats. For
     * example, `['assets/audio.mp3', 'assets/video.wav']`.
     * This is useful for ensuring that the audio can play across different
     * browsers with different capabilities. See
     * <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats" target="_blank">MDN</a>
     * for more information about supported formats.
     *
     * The second parameter, `callback`, is optional. It's a function to call once
     * the audio is ready to play.
     *
     * @method createAudio
     * @param  {String|String[]} [src] path to an audio file, or an array of paths
     *                                 for supporting different browsers.
     * @param  {Function} [callback]   function to call once the audio is ready to play.
     * @return {p5.MediaElement}       new <a href="#/p5.MediaElement">p5.MediaElement</a> object.
     *
     * @example
     * function setup() {
     *   noCanvas();
     *
     *   // Load the audio.
     *   let beat = createAudio('assets/beat.mp3');
     *
     *   // Show the default audio controls.
     *   beat.showControls();
     *
     *   describe('An audio beat plays when the user double-clicks the square.');
     * }
     */
    fn.createAudio = function (src, callback) {
      // p5._validateParameters('createAudio', arguments);
      return createMedia(this, AUDIO, src, callback);
    };

    /* CAMERA STUFF */

    fn.VIDEO = VIDEO;
    fn.AUDIO = AUDIO;

    // from: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
    // Older browsers might not implement mediaDevices at all, so we set an empty object first
    if (navigator.mediaDevices === undefined) {
      navigator.mediaDevices = {};
    }

    // Some browsers partially implement mediaDevices. We can't just assign an object
    // with getUserMedia as it would overwrite existing properties.
    // Here, we will just add the getUserMedia property if it's missing.
    if (navigator.mediaDevices.getUserMedia === undefined) {
      navigator.mediaDevices.getUserMedia = function (constraints) {
        // First get ahold of the legacy getUserMedia, if present
        const getUserMedia =
          navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

        // Some browsers just don't implement it - return a rejected promise with an error
        // to keep a consistent interface
        if (!getUserMedia) {
          return Promise.reject(
            new Error('getUserMedia is not implemented in this browser')
          );
        }

        // Otherwise, wrap the call to the old navigator.getUserMedia with a Promise
        return new Promise(function (resolve, reject) {
          getUserMedia.call(navigator, constraints, resolve, reject);
        });
      };
    }

    /**
     * Creates a `&lt;video&gt;` element that "captures" the audio/video stream from
     * the webcam and microphone.
     *
     * `createCapture()` returns a new
     * <a href="#/p5.MediaElement">p5.MediaElement</a> object. Videos are shown by
     * default. They can be hidden by calling `capture.hide()` and drawn to the
     * canvas using <a href="#/p5/image">image()</a>.
     *
     * The first parameter, `type`, is optional. It sets the type of capture to
     * use. By default, `createCapture()` captures both audio and video. If `VIDEO`
     * is passed, as in `createCapture(VIDEO)`, only video will be captured.
     * If `AUDIO` is passed, as in `createCapture(AUDIO)`, only audio will be
     * captured. A constraints object can also be passed to customize the stream.
     * See the <a href="http://w3c.github.io/mediacapture-main/getusermedia.html#media-track-constraints" target="_blank">
     * W3C documentation</a> for possible properties. Different browsers support different
     * properties.
     *
     * The 'flipped' property is an optional property which can be set to `{flipped:true}`
     * to mirror the video output.If it is true then it means that video will be mirrored
     * or flipped and if nothing is mentioned then by default it will be `false`.
     *
     * The second parameter,`callback`, is optional. It's a function to call once
     * the capture is ready for use. The callback function should have one
     * parameter, `stream`, that's a
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaStream" target="_blank">MediaStream</a> object.
     *
     * Note: `createCapture()` only works when running a sketch locally or using HTTPS. Learn more
     * <a href="http://stackoverflow.com/questions/34197653/getusermedia-in-chrome-47-without-using-https" target="_blank">here</a>
     * and <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia" target="_blank">here</a>.
     *
     * @method createCapture
     * @param  {(AUDIO|VIDEO|Object)}  [type] type of capture, either AUDIO or VIDEO,
     *                                   or a constraints object. Both video and audio
     *                                   audio streams are captured by default.
     * @param  {Object}                  [flipped] flip the capturing video and mirror the output with `{flipped:true}`. By
     *                                   default it is false.
     * @param  {Function}                [callback] function to call once the stream
     *                                   has loaded.
     * @return {p5.MediaElement} new <a href="#/p5.MediaElement">p5.MediaElement</a> object.
     *
     * @example
     * function setup() {
     *   noCanvas();
     *
     *   // Create the video capture.
     *   createCapture(VIDEO);
     *
     *   describe('A video stream from the webcam.');
     * }
     *
     * @example
     * let capture;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create the video capture and hide the element.
     *   capture = createCapture(VIDEO);
     *   capture.hide();
     *
     *   describe('A video stream from the webcam with inverted colors.');
     * }
     *
     * function draw() {
     *   // Draw the video capture within the canvas.
     *   image(capture, 0, 0, width, width * capture.height / capture.width);
     *
     *   // Invert the colors in the stream.
     *   filter(INVERT);
     * }
     *
     * @example
     * let capture;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create the video capture with mirrored output.
     *   capture = createCapture(VIDEO,{ flipped:true });
     *   capture.size(100,100);
     *
     *   describe('A video stream from the webcam with flipped or mirrored output.');
     * }
     *
     *
     * @example
     * function setup() {
     *   createCanvas(480, 120);
     *
     *   // Create a constraints object.
     *   let constraints = {
     *     video: {
     *       mandatory: {
     *         minWidth: 1280,
     *         minHeight: 720
     *       },
     *       optional: [{ maxFrameRate: 10 }]
     *     },
     *     audio: false
     *   };
     *
     *   // Create the video capture.
     *   createCapture(constraints);
     *
     *   describe('A video stream from the webcam.');
     * }
     */
    fn.createCapture = function (...args) {
      // p5._validateParameters('createCapture', args);

      // return if getUserMedia is not supported by the browser
      if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) {
        throw new DOMException('getUserMedia not supported in this browser');
      }

      let useVideo = true;
      let useAudio = true;
      let constraints;
      let callback;
      let flipped = false;

      for (const arg of args) {
        if (arg === fn.VIDEO) useAudio = false;
        else if (arg === fn.AUDIO) useVideo = false;
        else if (typeof arg === 'object') {
          if (arg.flipped !== undefined) {
            flipped = arg.flipped;
            delete arg.flipped;
          }
          constraints = Object.assign({}, constraints, arg);
        }
        else if (typeof arg === 'function') {
          callback = arg;
        }
      }

      const videoConstraints = { video: useVideo, audio: useAudio };
      constraints = Object.assign({}, videoConstraints, constraints);
      const domElement = document.createElement(VIDEO);
      // required to work in iOS 11 & up:
      domElement.setAttribute('playsinline', '');
      navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {
        try {
          if ('srcObject' in domElement) {
            domElement.srcObject = stream;
          } else {
            domElement.src = window.URL.createObjectURL(stream);
          }
        }
        catch (err) {
          domElement.src = stream;
        }
      }).catch(e => {
        if (e.name === 'NotFoundError')
          p5._friendlyError('No webcam found on this device', 'createCapture');
        if (e.name === 'NotAllowedError')
          p5._friendlyError('Access to the camera was denied', 'createCapture');

        console.error(e);
      });

      const videoEl = addElement(domElement, this, true);
      videoEl.loadedmetadata = false;
      // set width and height onload metadata
      domElement.addEventListener('loadedmetadata', function () {
        domElement.play();
        if (domElement.width) {
          videoEl.width = domElement.width;
          videoEl.height = domElement.height;
          if (flipped) {
            videoEl.elt.style.transform = 'scaleX(-1)';
          }
        } else {
          videoEl.width = videoEl.elt.width = domElement.videoWidth;
          videoEl.height = videoEl.elt.height = domElement.videoHeight;
        }
        videoEl.loadedmetadata = true;

        if (callback) callback(domElement.srcObject);
      });
      videoEl.flipped = flipped;
      return videoEl;
    };

    // =============================================================================
    //                         p5.MediaElement additions
    // =============================================================================

    /**
     * A class to handle audio and video.
     *
     * `p5.MediaElement` extends <a href="#/p5.Element">p5.Element</a> with
     * methods to handle audio and video. `p5.MediaElement` objects are created by
     * calling <a href="#/p5/createVideo">createVideo</a>,
     * <a href="#/p5/createAudio">createAudio</a>, and
     * <a href="#/p5/createCapture">createCapture</a>.
     *
     * @class p5.MediaElement
     * @param {String} elt DOM node that is wrapped
     * @extends p5.Element
     *
     * @example
     * let capture;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.MediaElement using createCapture().
     *   capture = createCapture(VIDEO);
     *   capture.hide();
     *
     *   describe('A webcam feed with inverted colors.');
     * }
     *
     * function draw() {
     *   // Display the video stream and invert the colors.
     *   image(capture, 0, 0, width, width * capture.height / capture.width);
     *   filter(INVERT);
     * }
     */
    p5.MediaElement = MediaElement;

    // Patch MediaElement to give it access to fn, which p5.sound may attach things to
    // if present in a sketch
    MediaElement.prototype._getSoundOut = function() {
      return p5.soundOut;
    };
    MediaElement.prototype._getAudioContext = function() {
      if (typeof fn.getAudioContext === 'function') {
        return fn.getAudioContext();
      } else {
        return undefined;
      }
    };

    /**
     * Path to the media element's source as a string.
     *
     * @for p5.MediaElement
     * @property src
     * @return {String} src
     * @example
     * let beat;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.MediaElement using createAudio().
     *   beat = createAudio('assets/beat.mp3');
     *
     *   describe('The text "https://p5js.org/reference/assets/beat.mp3" written in black on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   textWrap(CHAR);
     *   text(beat.src, 10, 10, 80, 80);
     * }
     */
  }

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

  /**
   * @requires constants
   */


  /*
    This function normalizes the first four arguments given to rect, ellipse and arc
    according to the mode.
    It returns a 'bounding box' object containing the coordinates of the upper left corner (x, y),
    and width and height (w, h). The returned width and height are always positive.
  */
  function modeAdjust(a, b, c, d, mode) {
    let bbox;

    if (mode === CORNER) {

      // CORNER mode already corresponds to a bounding box (top-left corner, width, height).
      // For negative widhts or heights, the absolute value is used.
      bbox = {
        x: a,
        y: b,
        w: Math.abs(c),
        h: Math.abs(d)
      };

    } else if (mode === CORNERS) {

      // CORNERS mode uses two opposite corners, in any configuration.
      // Make sure to get the top left corner by using the minimum of the x and y coordniates.
      bbox = {
        x: Math.min(a, c),
        y: Math.min(b, d),
        w: Math.abs(c - a),
        h: Math.abs(d - b)
      };

    } else if (mode === RADIUS) {

      // RADIUS mode uses the center point and half the width and height.
      // c (half width) and d (half height) could be negative, so use the absolute value
      // in calculating the top left corner (x, y).
      c = Math.abs(c);
      d = Math.abs(d);
      bbox = {
        x: a - c,
        y: b - d,
        w: 2 * c,
        h: 2 * d
      };

    } else if (mode === CENTER) {

      // CENTER mode uses the center point, width and height.
      // c (width) and d (height) could be negative, so use the absolute value
      // in calculating the top-left corner (x, y).
      c = Math.abs(c);
      d = Math.abs(d);
      bbox = {
        x: a - (c * 0.5),
        y: b - (d * 0.5),
        w: c,
        h: d
      };

    }

    return bbox;
  }

  var canvas = { modeAdjust };

  /**
   * @module Shape
   * @submodule 2D Primitives
   * @for p5
   * @requires core
   * @requires constants
   */


  function primitives(p5, fn){
    /**
     * This function does 3 things:
     *
     *   1. Bounds the desired start/stop angles for an arc (in radians) so that:
     *
     *          0 <= start < TWO_PI ;    start <= stop < start + TWO_PI
     *
     *      This means that the arc rendering functions don't have to be concerned
     *      with what happens if stop is smaller than start, or if the arc 'goes
     *      round more than once', etc.: they can just start at start and increase
     *      until stop and the correct arc will be drawn.
     *
     *   2. Optionally adjusts the angles within each quadrant to counter the naive
     *      scaling of the underlying ellipse up from the unit circle.  Without
     *      this, the angles become arbitrary when width != height: 45 degrees
     *      might be drawn at 5 degrees on a 'wide' ellipse, or at 85 degrees on
     *      a 'tall' ellipse.
     *
     *   3. Flags up when start and stop correspond to the same place on the
     *      underlying ellipse.  This is useful if you want to do something special
     *      there (like rendering a whole ellipse instead).
     */
    fn._normalizeArcAngles = (
      start,
      stop,
      width,
      height,
      correctForScaling
    ) => {
      const epsilon = 0.00001; // Smallest visible angle on displays up to 4K.
      let separation;

      // The order of the steps is important here: each one builds upon the
      // adjustments made in the steps that precede it.

      // Constrain both start and stop to [0,TWO_PI).
      start = start - TWO_PI * Math.floor(start / TWO_PI);
      stop = stop - TWO_PI * Math.floor(stop / TWO_PI);

      // Get the angular separation between the requested start and stop points.
      //
      // Technically this separation only matches what gets drawn if
      // correctForScaling is enabled.  We could add a more complicated calculation
      // for when the scaling is uncorrected (in which case the drawn points could
      // end up pushed together or pulled apart quite dramatically relative to what
      // was requested), but it would make things more opaque for little practical
      // benefit.
      //
      // (If you do disable correctForScaling and find that correspondToSamePoint
      // is set too aggressively, the easiest thing to do is probably to just make
      // epsilon smaller...)
      separation = Math.min(
        Math.abs(start - stop),
        TWO_PI - Math.abs(start - stop)
      );

      // Optionally adjust the angles to counter linear scaling.
      if (correctForScaling) {
        if (start <= HALF_PI) {
          start = Math.atan(width / height * Math.tan(start));
        } else if (start > HALF_PI && start <= 3 * HALF_PI) {
          start = Math.atan(width / height * Math.tan(start)) + PI;
        } else {
          start = Math.atan(width / height * Math.tan(start)) + TWO_PI;
        }
        if (stop <= HALF_PI) {
          stop = Math.atan(width / height * Math.tan(stop));
        } else if (stop > HALF_PI && stop <= 3 * HALF_PI) {
          stop = Math.atan(width / height * Math.tan(stop)) + PI;
        } else {
          stop = Math.atan(width / height * Math.tan(stop)) + TWO_PI;
        }
      }

      // Ensure that start <= stop < start + TWO_PI.
      if (start > stop) {
        stop += TWO_PI;
      }

      return {
        start,
        stop,
        correspondToSamePoint: separation < epsilon
      };
    };

    /**
     * Draws an arc.
     *
     * An arc is a section of an ellipse defined by the `x`, `y`, `w`, and
     * `h` parameters. `x` and `y` set the location of the arc's center. `w` and
     * `h` set the arc's width and height. See
     * <a href="#/p5/ellipse">ellipse()</a> and
     * <a href="#/p5/ellipseMode">ellipseMode()</a> for more details.
     *
     * The fifth and sixth parameters, `start` and `stop`, set the angles
     * between which to draw the arc. Arcs are always drawn clockwise from
     * `start` to `stop`. Angles are always given in radians.
     *
     * The seventh parameter, `mode`, is optional. It determines the arc's fill
     * style. The fill modes are a semi-circle (`OPEN`), a closed semi-circle
     * (`CHORD`), or a closed pie segment (`PIE`).
     *
     * The eighth parameter, `detail`, is also optional. It determines how many
     * vertices are used to draw the arc in WebGL mode. The default value is 25.
     *
     * @method arc
     * @param  {Number} x      x-coordinate of the arc's ellipse.
     * @param  {Number} y      y-coordinate of the arc's ellipse.
     * @param  {Number} w      width of the arc's ellipse by default.
     * @param  {Number} h      height of the arc's ellipse by default.
     * @param  {Number} start  angle to start the arc, specified in radians.
     * @param  {Number} stop   angle to stop the arc, specified in radians.
     * @param  {(CHORD|PIE|OPEN)} [mode] optional parameter to determine the way of drawing
     *                         the arc. either CHORD, PIE, or OPEN.
     * @param  {Integer} [detail] optional parameter for WebGL mode only. This is to
     *                         specify the number of vertices that makes up the
     *                         perimeter of the arc. Default value is 25. Won't
     *                         draw a stroke for a detail of more than 50.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   arc(50, 50, 80, 80, 0, PI + HALF_PI);
     *
     *   describe('A white circle on a gray canvas. The top-right quarter of the circle is missing.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   arc(50, 50, 80, 40, 0, PI + HALF_PI);
     *
     *   describe('A white ellipse on a gray canvas. The top-right quarter of the ellipse is missing.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Bottom-right.
     *   arc(50, 55, 50, 50, 0, HALF_PI);
     *
     *   noFill();
     *
     *   // Bottom-left.
     *   arc(50, 55, 60, 60, HALF_PI, PI);
     *
     *   // Top-left.
     *   arc(50, 55, 70, 70, PI, PI + QUARTER_PI);
     *
     *   // Top-right.
     *   arc(50, 55, 80, 80, PI + QUARTER_PI, TWO_PI);
     *
     *   describe(
     *     'A shattered outline of an circle with a quarter of a white circle at the bottom-right.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Default fill mode.
     *   arc(50, 50, 80, 80, 0, PI + QUARTER_PI);
     *
     *   describe('A white circle with the top-right third missing. The bottom is outlined in black.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // OPEN fill mode.
     *   arc(50, 50, 80, 80, 0, PI + QUARTER_PI, OPEN);
     *
     *   describe(
     *     'A white circle missing a section from the top-right. The bottom is outlined in black.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // CHORD fill mode.
     *   arc(50, 50, 80, 80, 0, PI + QUARTER_PI, CHORD);
     *
     *   describe('A white circle with a black outline missing a section from the top-right.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // PIE fill mode.
     *   arc(50, 50, 80, 80, 0, PI + QUARTER_PI, PIE);
     *
     *   describe('A white circle with a black outline. The top-right third is missing.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   // PIE fill mode.
     *   arc(0, 0, 80, 80, 0, PI + QUARTER_PI, PIE);
     *
     *   describe('A white circle with a black outline. The top-right third is missing.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   // PIE fill mode with 5 vertices.
     *   arc(0, 0, 80, 80, 0, PI + QUARTER_PI, PIE, 5);
     *
     *   describe('A white circle with a black outline. The top-right third is missing.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A yellow circle on a black background. The circle opens and closes its mouth.');
     * }
     *
     * function draw() {
     *   background(0);
     *
     *   // Style the arc.
     *   noStroke();
     *   fill(255, 255, 0);
     *
     *   // Update start and stop angles.
     *   let biteSize = PI / 16;
     *   let startAngle = biteSize * sin(frameCount * 0.1) + biteSize;
     *   let endAngle = TWO_PI - startAngle;
     *
     *   // Draw the arc.
     *   arc(50, 50, 80, 80, startAngle, endAngle, PIE);
     * }
     */
    fn.arc = function(x, y, w, h, start, stop, mode, detail) {
      // this.validate("p5.arc", arguments);
      // p5._validateParameters('arc', arguments);

      // if the current stroke and fill settings wouldn't result in something
      // visible, exit immediately
      if (
        !this._renderer.states.strokeColor &&
        !this._renderer.states.fillColor
      ) {
        return this;
      }

      if (start === stop) {
        return this;
      }

      start = this._toRadians(start);
      stop = this._toRadians(stop);

      const vals = canvas.modeAdjust(
        x, y,
        w, h,
        this._renderer.states.ellipseMode
      );
      const angles = this._normalizeArcAngles(start, stop, vals.w, vals.h, true);

      if (angles.correspondToSamePoint) {
        // If the arc starts and ends at (near enough) the same place, we choose to
        // draw an ellipse instead.  This is preferable to faking an ellipse (by
        // making stop ever-so-slightly less than start + TWO_PI) because the ends
        // join up to each other rather than at a vertex at the centre (leaving
        // an unwanted spike in the stroke/fill).
        this._renderer.ellipse([vals.x, vals.y, vals.w, vals.h, detail]);
      } else {
        this._renderer.arc(
          vals.x,
          vals.y,
          vals.w,
          vals.h,
          angles.start, // [0, TWO_PI)
          angles.stop, // [start, start + TWO_PI)
          mode,
          detail
        );

        //accessible Outputs
        if (this._accessibleOutputs.grid || this._accessibleOutputs.text) {
          this._accsOutput('arc', [
            vals.x,
            vals.y,
            vals.w,
            vals.h,
            angles.start,
            angles.stop,
            mode
          ]);
        }
      }

      return this;
    };

    /**
     * Draws an ellipse (oval).
     *
     * An ellipse is a round shape defined by the `x`, `y`, `w`, and
     * `h` parameters. `x` and `y` set the location of its center. `w` and
     * `h` set its width and height. See
     * <a href="#/p5/ellipseMode">ellipseMode()</a> for other ways to set
     * its position.
     *
     * If no height is set, the value of width is used for both the width and
     * height. If a negative height or width is specified, the absolute value is
     * taken.
     *
     * The fifth parameter, `detail`, is also optional. It determines how many
     * vertices are used to draw the ellipse in WebGL mode. The default value is
     * 25.
     *
     * @method ellipse
     * @param  {Number} x x-coordinate of the center of the ellipse.
     * @param  {Number} y y-coordinate of the center of the ellipse.
     * @param  {Number} w width of the ellipse.
     * @param  {Number} [h] height of the ellipse.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   ellipse(50, 50, 80, 80);
     *
     *   describe('A white circle on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   ellipse(50, 50, 80);
     *
     *   describe('A white circle on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   ellipse(50, 50, 80, 40);
     *
     *   describe('A white ellipse on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   ellipse(0, 0, 80, 40);
     *
     *   describe('A white ellipse on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   // Use 6 vertices.
     *   ellipse(0, 0, 80, 40, 6);
     *
     *   describe('A white hexagon on a gray canvas.');
     * }
     */
    /**
     * @method ellipse
     * @param  {Number} x
     * @param  {Number} y
     * @param  {Number} w
     * @param  {Number} h
     * @param  {Integer} [detail] optional parameter for WebGL mode only. This is to
     *                         specify the number of vertices that makes up the
     *                         perimeter of the ellipse. Default value is 25. Won't
     *                         draw a stroke for a detail of more than 50.
     */
    fn.ellipse = function(x, y, w, h, detailX) {
      // p5._validateParameters('ellipse', arguments);
      return this._renderEllipse(...arguments);
    };

    /**
     * Draws a circle.
     *
     * A circle is a round shape defined by the `x`, `y`, and `d` parameters.
     * `x` and `y` set the location of its center. `d` sets its width and height (diameter).
     * Every point on the circle's edge is the same distance, `0.5 * d`, from its center.
     * `0.5 * d` (half the diameter) is the circle's radius.
     * See <a href="#/p5/ellipseMode">ellipseMode()</a> for other ways to set its position.
     *
     * @method circle
     * @param  {Number} x  x-coordinate of the center of the circle.
     * @param  {Number} y  y-coordinate of the center of the circle.
     * @param  {Number} d  diameter of the circle.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   circle(50, 50, 25);
     *
     *   describe('A white circle with black outline in the middle of a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   circle(0, 0, 25);
     *
     *   describe('A white circle with black outline in the middle of a gray canvas.');
     * }
     */
    fn.circle = function(...args) {
      // p5._validateParameters('circle', args);
      const argss = args.slice( 0, 2);
      argss.push(args[2], args[2]);
      return this._renderEllipse(...argss);
    };

    // internal method for drawing ellipses (without parameter validation)
    fn._renderEllipse = function(x, y, w, h, detailX) {
      // if the current stroke and fill settings wouldn't result in something
      // visible, exit immediately
      if (
        !this._renderer.states.strokeColor &&
        !this._renderer.states.fillColor
      ) {
        return this;
      }

      // Duplicate 3rd argument if only 3 given.
      if (typeof h === 'undefined') {
        h = w;
      }

      const vals = canvas.modeAdjust(
        x, y,
        w, h,
        this._renderer.states.ellipseMode
      );
      this._renderer.ellipse([vals.x, vals.y, vals.w, vals.h, detailX]);

      //accessible Outputs
      if (this._accessibleOutputs.grid || this._accessibleOutputs.text) {
        this._accsOutput('ellipse', [vals.x, vals.y, vals.w, vals.h]);
      }

      return this;
    };

    /**
     * Draws a straight line between two points.
     *
     * A line's default width is one pixel. The version of `line()` with four
     * parameters draws the line in 2D. To color a line, use the
     * <a href="#/p5/stroke">stroke()</a> function. To change its width, use the
     * <a href="#/p5/strokeWeight">strokeWeight()</a> function. A line
     * can't be filled, so the <a href="#/p5/fill">fill()</a> function won't
     * affect the line's color.
     *
     * The version of `line()` with six parameters allows the line to be drawn in
     * 3D space. Doing so requires adding the `WEBGL` argument to
     * <a href="#/p5/createCanvas">createCanvas()</a>.
     *
     * @method line
     * @param  {Number} x1 the x-coordinate of the first point.
     * @param  {Number} y1 the y-coordinate of the first point.
     * @param  {Number} x2 the x-coordinate of the second point.
     * @param  {Number} y2 the y-coordinate of the second point.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   line(30, 20, 85, 75);
     *
     *   describe(
     *     'A black line on a gray canvas running from top-center to bottom-right.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the line.
     *   stroke('magenta');
     *   strokeWeight(5);
     *
     *   line(30, 20, 85, 75);
     *
     *   describe(
     *     'A thick, magenta line on a gray canvas running from top-center to bottom-right.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Top.
     *   line(30, 20, 85, 20);
     *
     *   // Right.
     *   stroke(126);
     *   line(85, 20, 85, 75);
     *
     *   // Bottom.
     *   stroke(255);
     *   line(85, 75, 30, 75);
     *
     *   describe(
     *     'Three lines drawn in grayscale on a gray canvas. They form the top, right, and bottom sides of a square.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   line(-20, -30, 35, 25);
     *
     *   describe(
     *     'A black line on a gray canvas running from top-center to bottom-right.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A black line connecting two spheres. The scene spins slowly.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Rotate around the y-axis.
     *   rotateY(frameCount * 0.01);
     *
     *   // Draw a line.
     *   line(0, 0, 0, 30, 20, -10);
     *
     *   // Draw the center sphere.
     *   sphere(10);
     *
     *   // Translate to the second point.
     *   translate(30, 20, -10);
     *
     *   // Draw the bottom-right sphere.
     *   sphere(10);
     * }
     */

    /**
     * @method line
     * @param  {Number} x1
     * @param  {Number} y1
     * @param  {Number} z1 the z-coordinate of the first point.
     * @param  {Number} x2
     * @param  {Number} y2
     * @param  {Number} z2 the z-coordinate of the second point.
     * @chainable
     */
    fn.line = function(...args) {
      // p5._validateParameters('line', args);

      if (this._renderer.states.strokeColor) {
        this._renderer.line(...args);
      }

      //accessible Outputs
      if (this._accessibleOutputs.grid || this._accessibleOutputs.text) {
        this._accsOutput('line', args);
      }

      return this;
    };

    /**
     * Draws a single point in space.
     *
     * A point's default width is one pixel. To color a point, use the
     * <a href="#/p5/stroke">stroke()</a> function. To change its width, use the
     * <a href="#/p5/strokeWeight">strokeWeight()</a> function. A point
     * can't be filled, so the <a href="#/p5/fill">fill()</a> function won't
     * affect the point's color.
     *
     * The version of `point()` with two parameters allows the point's location to
     * be set with its x- and y-coordinates, as in `point(10, 20)`.
     *
     * The version of `point()` with three parameters allows the point to be drawn
     * in 3D space with x-, y-, and z-coordinates, as in `point(10, 20, 30)`.
     * Doing so requires adding the `WEBGL` argument to
     * <a href="#/p5/createCanvas">createCanvas()</a>.
     *
     * The version of `point()` with one parameter allows the point's location to
     * be set with a <a href="#/p5/p5.Vector">p5.Vector</a> object.
     *
     * @method point
     * @param  {Number} x the x-coordinate.
     * @param  {Number} y the y-coordinate.
     * @param  {Number} [z] the z-coordinate (for WebGL mode).
     * @chainable
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Making point to 5 pixels
     *   strokeWeight(5);
     *
     *   // Top-left.
     *   point(30, 20);
     *
     *   // Top-right.
     *   point(85, 20);
     *
     *   // Bottom-right.
     *   point(85, 75);
     *
     *   // Bottom-left.
     *   point(30, 75);
     *
     *   describe(
     *     'Four small, black points drawn on a gray canvas. The points form the corners of a square.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Making point to 5 pixels.
     *   strokeWeight(5);
     *
     *   // Top-left.
     *   point(30, 20);
     *
     *   // Top-right.
     *   point(70, 20);
     *
     *   // Style the next points.
     *   stroke('purple');
     *   strokeWeight(10);
     *
     *   // Bottom-right.
     *   point(70, 80);
     *
     *   // Bottom-left.
     *   point(30, 80);
     *
     *   describe(
     *     'Four points drawn on a gray canvas. Two are black and two are purple. The points form the corners of a square.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     * 
     *   // Making point to 5 pixels.
     *   strokeWeight(5);
     *
     *   // Top-left.
     *   let a = createVector(30, 20);
     *   point(a);
     *
     *   // Top-right.
     *   let b = createVector(70, 20);
     *   point(b);
     *
     *   // Bottom-right.
     *   let c = createVector(70, 80);
     *   point(c);
     *
     *   // Bottom-left.
     *   let d = createVector(30, 80);
     *   point(d);
     *
     *   describe(
     *     'Four small, black points drawn on a gray canvas. The points form the corners of a square.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('Two purple points drawn on a gray canvas.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the points.
     *   stroke('purple');
     *   strokeWeight(10);
     *
     *   // Top-left.
     *   point(-20, -30);
     *
     *   // Bottom-right.
     *   point(20, 30);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('Two purple points drawn on a gray canvas. The scene spins slowly.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Rotate around the y-axis.
     *   rotateY(frameCount * 0.01);
     *
     *   // Style the points.
     *   stroke('purple');
     *   strokeWeight(10);
     *
     *   // Top-left.
     *   point(-20, -30, 0);
     *
     *   // Bottom-right.
     *   point(20, 30, -50);
     * }
     */

    /**
     * @method point
     * @param {p5.Vector} coordinateVector the coordinate vector.
     * @chainable
     */
    fn.point = function(...args) {
      // p5._validateParameters('point', args);

      if (this._renderer.states.strokeColor) {
        if (args.length === 1 && args[0] instanceof p5.Vector) {
          this._renderer.point.call(
            this._renderer,
            args[0].x,
            args[0].y,
            args[0].z
          );
        } else {
          this._renderer.point(...args);
          //accessible Outputs
          if (this._accessibleOutputs.grid || this._accessibleOutputs.text) {
            this._accsOutput('point', args);
          }
        }
      }

      return this;
    };

    /**
     * Draws a quadrilateral (four-sided shape).
     *
     * Quadrilaterals include rectangles, squares, rhombuses, and trapezoids. The
     * first pair of parameters `(x1, y1)` sets the quad's first point. The next
     * three pairs of parameters set the coordinates for its next three points
     * `(x2, y2)`, `(x3, y3)`, and `(x4, y4)`. Points should be added in either
     * clockwise or counter-clockwise order.
     *
     * The version of `quad()` with twelve parameters allows the quad to be drawn
     * in 3D space. Doing so requires adding the `WEBGL` argument to
     * <a href="#/p5/createCanvas">createCanvas()</a>.
     *
     * The thirteenth and fourteenth parameters are optional. In WebGL mode, they
     * set the number of segments used to draw the quadrilateral in the x- and
     * y-directions. They're both 2 by default.
     *
     * @method quad
     * @param {Number} x1 the x-coordinate of the first point.
     * @param {Number} y1 the y-coordinate of the first point.
     * @param {Number} x2 the x-coordinate of the second point.
     * @param {Number} y2 the y-coordinate of the second point.
     * @param {Number} x3 the x-coordinate of the third point.
     * @param {Number} y3 the y-coordinate of the third point.
     * @param {Number} x4 the x-coordinate of the fourth point.
     * @param {Number} y4 the y-coordinate of the fourth point.
     * @param {Integer} [detailX] number of segments in the x-direction.
     * @param {Integer} [detailY] number of segments in the y-direction.
     * @chainable
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   quad(20, 20, 80, 20, 80, 80, 20, 80);
     *
     *   describe('A white square with a black outline drawn on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   quad(20, 30, 80, 30, 80, 70, 20, 70);
     *
     *   describe('A white rectangle with a black outline drawn on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   quad(50, 62, 86, 50, 50, 38, 14, 50);
     *
     *   describe('A white rhombus with a black outline drawn on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   quad(20, 50, 80, 30, 80, 70, 20, 70);
     *
     *   describe('A white trapezoid with a black outline drawn on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   quad(-30, -30, 30, -30, 30, 30, -30, 30);
     *
     *   describe('A white square with a black outline drawn on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A wavy white surface spins around on gray canvas.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Rotate around the y-axis.
     *   rotateY(frameCount * 0.01);
     *
     *   // Draw the quad.
     *   quad(-30, -30, 0, 30, -30, 0, 30, 30, 20, -30, 30, -20);
     * }
     */
    /**
     * @method quad
     * @param {Number} x1
     * @param {Number} y1
     * @param {Number} z1 the z-coordinate of the first point.
     * @param {Number} x2
     * @param {Number} y2
     * @param {Number} z2 the z-coordinate of the second point.
     * @param {Number} x3
     * @param {Number} y3
     * @param {Number} z3 the z-coordinate of the third point.
     * @param {Number} x4
     * @param {Number} y4
     * @param {Number} z4 the z-coordinate of the fourth point.
     * @param {Integer} [detailX]
     * @param {Integer} [detailY]
     * @chainable
     */
    fn.quad = function(...args) {
      // p5._validateParameters('quad', args);

      if (this._renderer.states.strokeColor || this._renderer.states.fillColor) {
        if (this._renderer.isP3D && args.length < 12) {
          // if 3D and we weren't passed 12 args, assume Z is 0
          this._renderer.quad.call(
            this._renderer,
            args[0], args[1], 0,
            args[2], args[3], 0,
            args[4], args[5], 0,
            args[6], args[7], 0,
            args[8], args[9]);
        } else {
          this._renderer.quad(...args);
          //accessibile outputs
          if (this._accessibleOutputs.grid || this._accessibleOutputs.text) {
            this._accsOutput('quadrilateral', args);
          }
        }
      }

      return this;
    };

    /**
     * Draws a rectangle.
     *
     * A rectangle is a four-sided shape defined by the `x`, `y`, `w`, and `h`
     * parameters. `x` and `y` set the location of its top-left corner. `w` sets
     * its width and `h` sets its height. Every angle in the rectangle measures
     * 90˚. See <a href="#/p5/rectMode">rectMode()</a> for other ways to define
     * rectangles.
     *
     * The version of `rect()` with five parameters creates a rounded rectangle. The
     * fifth parameter sets the radius for all four corners.
     *
     * The version of `rect()` with eight parameters also creates a rounded
     * rectangle. Each of the last four parameters set the radius of a corner. The
     * radii start with the top-left corner and move clockwise around the
     * rectangle. If any of these parameters are omitted, they are set to the
     * value of the last radius that was set.
     *
     * @method rect
     * @param  {Number} x  x-coordinate of the rectangle.
     * @param  {Number} y  y-coordinate of the rectangle.
     * @param  {Number} w  width of the rectangle.
     * @param  {Number} [h]  height of the rectangle.
     * @param  {Number} [tl] optional radius of top-left corner.
     * @param  {Number} [tr] optional radius of top-right corner.
     * @param  {Number} [br] optional radius of bottom-right corner.
     * @param  {Number} [bl] optional radius of bottom-left corner.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   rect(30, 20, 55, 55);
     *
     *   describe('A white square with a black outline on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   rect(30, 20, 55, 40);
     *
     *   describe('A white rectangle with a black outline on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Give all corners a radius of 20.
     *   rect(30, 20, 55, 50, 20);
     *
     *   describe('A white rectangle with a black outline and round edges on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Give each corner a unique radius.
     *   rect(30, 20, 55, 50, 20, 15, 10, 5);
     *
     *   describe('A white rectangle with a black outline and round edges of different radii.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   rect(-20, -30, 55, 55);
     *
     *   describe('A white square with a black outline on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white square spins around on gray canvas.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Rotate around the y-axis.
     *   rotateY(frameCount * 0.01);
     *
     *   // Draw the rectangle.
     *   rect(-20, -30, 55, 55);
     * }
     */

    /**
     * @method rect
     * @param  {Number} x
     * @param  {Number} y
     * @param  {Number} w
     * @param  {Number} h
     * @param  {Integer} [detailX] number of segments in the x-direction (for WebGL mode).
     * @param  {Integer} [detailY] number of segments in the y-direction (for WebGL mode).
     * @chainable
     */
    fn.rect = function(...args) {
      // p5._validateParameters('rect', args);
      return this._renderRect(...args);
    };

    /**
     * Draws a square.
     *
     * A square is a four-sided shape defined by the `x`, `y`, and `s`
     * parameters. `x` and `y` set the location of its top-left corner. `s` sets
     * its width and height. Every angle in the square measures 90˚ and all its
     * sides are the same length. See <a href="#/p5/rectMode">rectMode()</a> for
     * other ways to define squares.
     *
     * The version of `square()` with four parameters creates a rounded square.
     * The fourth parameter sets the radius for all four corners.
     *
     * The version of `square()` with seven parameters also creates a rounded
     * square. Each of the last four parameters set the radius of a corner. The
     * radii start with the top-left corner and move clockwise around the
     * square. If any of these parameters are omitted, they are set to the
     * value of the last radius that was set.
     *
     * @method square
     * @param  {Number} x  x-coordinate of the square.
     * @param  {Number} y  y-coordinate of the square.
     * @param  {Number} s  side size of the square.
     * @param  {Number} [tl] optional radius of top-left corner.
     * @param  {Number} [tr] optional radius of top-right corner.
     * @param  {Number} [br] optional radius of bottom-right corner.
     * @param  {Number} [bl] optional radius of bottom-left corner.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   square(30, 20, 55);
     *
     *   describe('A white square with a black outline in on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Give all corners a radius of 20.
     *   square(30, 20, 55, 20);
     *
     *   describe(
     *     'A white square with a black outline and round edges on a gray canvas.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Give each corner a unique radius.
     *   square(30, 20, 55, 20, 15, 10, 5);
     *
     *   describe('A white square with a black outline and round edges of different radii.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   square(-20, -30, 55);
     *
     *   describe('A white square with a black outline in on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white square spins around on gray canvas.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Rotate around the y-axis.
     *   rotateY(frameCount * 0.01);
     *
     *   // Draw the square.
     *   square(-20, -30, 55);
     * }
     */
    fn.square = function(x, y, s, tl, tr, br, bl) {
      // p5._validateParameters('square', arguments);
      // duplicate width for height in case of square
      return this._renderRect.call(this, x, y, s, s, tl, tr, br, bl);
    };

    // internal method to have renderer draw a rectangle
    fn._renderRect = function() {
      if (this._renderer.states.strokeColor || this._renderer.states.fillColor) {
        // duplicate width for height in case only 3 arguments is provided
        if (arguments.length === 3) {
          arguments[3] = arguments[2];
        }
        const vals = canvas.modeAdjust(
          arguments[0],
          arguments[1],
          arguments[2],
          arguments[3],
          this._renderer.states.rectMode
        );

        // For the default rectMode (CORNER), restore a possible negative width/height
        // removed by modeAdjust(). This results in flipped/mirrored rendering,
        // which is especially noticable when using WEGBL rendering and texture().
        // Note that this behavior only applies to rect(), NOT to ellipse() and arc().
        if (this._renderer.states.rectMode === CORNER) {
          vals.w = arguments[2];
          vals.h = arguments[3];
        }

        const args = [vals.x, vals.y, vals.w, vals.h];
        // append the additional arguments (either cornder radii, or
        // segment details) to the argument list
        for (let i = 4; i < arguments.length; i++) {
          args[i] = arguments[i];
        }
        this._renderer.rect(args);

        //accessible outputs
        if (this._accessibleOutputs.grid || this._accessibleOutputs.text) {
          this._accsOutput('rectangle', [vals.x, vals.y, vals.w, vals.h]);
        }
      }

      return this;
    };

    /**
     * Draws a triangle.
     *
     * A triangle is a three-sided shape defined by three points. The
     * first two parameters specify the triangle's first point `(x1, y1)`. The
     * middle two parameters specify its second point `(x2, y2)`. And the last two
     * parameters specify its third point `(x3, y3)`.
     *
     * @method triangle
     * @param  {Number} x1 x-coordinate of the first point.
     * @param  {Number} y1 y-coordinate of the first point.
     * @param  {Number} x2 x-coordinate of the second point.
     * @param  {Number} y2 y-coordinate of the second point.
     * @param  {Number} x3 x-coordinate of the third point.
     * @param  {Number} y3 y-coordinate of the third point.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   triangle(30, 75, 58, 20, 86, 75);
     *
     *   describe('A white triangle with a black outline on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   triangle(-20, 25, 8, -30, 36, 25);
     *
     *   describe('A white triangle with a black outline on a gray canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white triangle spins around on a gray canvas.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Rotate around the y-axis.
     *   rotateY(frameCount * 0.01);
     *
     *   // Draw the triangle.
     *   triangle(-20, 25, 8, -30, 36, 25);
     * }
     */
    fn.triangle = function(...args) {
      // p5._validateParameters('triangle', args);

      if (this._renderer.states.strokeColor || this._renderer.states.fillColor) {
        this._renderer.triangle(args);
      }

      //accessible outputs
      if (this._accessibleOutputs.grid || this._accessibleOutputs.text) {
        this._accsOutput('triangle', args);
      }

      return this;
    };
  }

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

  /**
   * @module Shape
   * @submodule Attributes
   * @for p5
   * @requires core
   * @requires constants
   */


  function attributes(p5, fn){
    /**
     * Changes where ellipses, circles, and arcs are drawn.
     *
     * By default, the first two parameters of
     * <a href="#/p5/ellipse">ellipse()</a>, <a href="#/p5/circle">circle()</a>,
     * and <a href="#/p5/arc">arc()</a>
     * are the x- and y-coordinates of the shape's center. The next parameters set
     * the shape's width and height. This is the same as calling
     * `ellipseMode(CENTER)`.
     *
     * `ellipseMode(RADIUS)` also uses the first two parameters to set the x- and
     * y-coordinates of the shape's center. The next parameters are half of the
     * shapes's width and height. Calling `ellipse(0, 0, 10, 15)` draws a shape
     * with a width of 20 and height of 30.
     *
     * `ellipseMode(CORNER)` uses the first two parameters as the upper-left
     * corner of the shape. The next parameters are its width and height.
     *
     * `ellipseMode(CORNERS)` uses the first two parameters as the location of one
     * corner of the ellipse's bounding box. The next parameters are the location
     * of the opposite corner.
     *
     * The argument passed to `ellipseMode()` must be written in ALL CAPS because
     * the constants `CENTER`, `RADIUS`, `CORNER`, and `CORNERS` are defined this
     * way. JavaScript is a case-sensitive language.
     *
     * @method ellipseMode
     * @param  {(CENTER|RADIUS|CORNER|CORNERS)} mode either CENTER, RADIUS, CORNER, or CORNERS
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // White ellipse.
     *   ellipseMode(RADIUS);
     *   fill(255);
     *   ellipse(50, 50, 30, 30);
     *
     *   // Gray ellipse.
     *   ellipseMode(CENTER);
     *   fill(100);
     *   ellipse(50, 50, 30, 30);
     *
     *   describe('A white circle with a gray circle at its center. Both circles have black outlines.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // White ellipse.
     *   ellipseMode(CORNER);
     *   fill(255);
     *   ellipse(25, 25, 50, 50);
     *
     *   // Gray ellipse.
     *   ellipseMode(CORNERS);
     *   fill(100);
     *   ellipse(25, 25, 50, 50);
     *
     *   describe('A white circle with a gray circle at its top-left corner. Both circles have black outlines.');
     * }
     */
    fn.ellipseMode = function(m) {
      // p5._validateParameters('ellipseMode', arguments);
      if (
        m === CORNER ||
        m === CORNERS ||
        m === RADIUS ||
        m === CENTER
      ) {
        this._renderer.states.setValue('ellipseMode', m);
      }
      return this;
    };

    /**
     * Draws certain features with jagged (aliased) edges.
     *
     * <a href="#/p5/smooth">smooth()</a> is active by default. In 2D mode,
     * `noSmooth()` is helpful for scaling up images without blurring. The
     * functions don't affect shapes or fonts.
     *
     * In WebGL mode, `noSmooth()` causes all shapes to be drawn with jagged
     * (aliased) edges. The functions don't affect images or fonts.
     *
     * @method noSmooth
     * @chainable
     *
     * @example
     * let heart;
     *
     * async function setup() {
     *   // Load a pixelated heart image from an image data string.
     *   heart = await loadImage('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAAXNSR0IArs4c6QAAAEZJREFUGFd9jcsNACAIQ9tB2MeR3YdBMBBq8CIXPi2vBICIiOwkOedatllqWO6Y8yOWoyuNf1GZwgmf+RRG2YXr+xVFmA8HZ9Mx/KGPMtcAAAAASUVORK5CYII=');
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Antialiased hearts.
     *   image(heart, 10, 10);
     *   image(heart, 20, 10, 16, 16);
     *   image(heart, 40, 10, 32, 32);
     *
     *   // Aliased hearts.
     *   noSmooth();
     *   image(heart, 10, 60);
     *   image(heart, 20, 60, 16, 16);
     *   image(heart, 40, 60, 32, 32);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   circle(0, 0, 80);
     *
     *   describe('A white circle on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Disable smoothing.
     *   noSmooth();
     *
     *   background(200);
     *
     *   circle(0, 0, 80);
     *
     *   describe('A pixelated white circle on a gray background.');
     * }
     */
    fn.noSmooth = function() {
      if (!this._renderer.isP3D) {
        if ('imageSmoothingEnabled' in this.drawingContext) {
          this.drawingContext.imageSmoothingEnabled = false;
        }
      } else {
        this.setAttributes('antialias', false);
      }
      return this;
    };

    /**
     * Changes where rectangles and squares are drawn.
     *
     * By default, the first two parameters of
     * <a href="#/p5/rect">rect()</a> and <a href="#/p5/square">square()</a>,
     * are the x- and y-coordinates of the shape's upper left corner. The next parameters set
     * the shape's width and height. This is the same as calling
     * `rectMode(CORNER)`.
     *
     * `rectMode(CORNERS)` also uses the first two parameters as the location of
     * one of the corners. The next parameters are the location of the opposite
     * corner. This mode only works for <a href="#/p5/rect">rect()</a>.
     *
     * `rectMode(CENTER)` uses the first two parameters as the x- and
     * y-coordinates of the shape's center. The next parameters are its width and
     * height.
     *
     * `rectMode(RADIUS)` also uses the first two parameters as the x- and
     * y-coordinates of the shape's center. The next parameters are
     * half of the shape's width and height.
     *
     * The argument passed to `rectMode()` must be written in ALL CAPS because the
     * constants `CENTER`, `RADIUS`, `CORNER`, and `CORNERS` are defined this way.
     * JavaScript is a case-sensitive language.
     *
     * @method rectMode
     * @param  {(CENTER|RADIUS|CORNER|CORNERS)} mode either CORNER, CORNERS, CENTER, or RADIUS
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   rectMode(CORNER);
     *   fill(255);
     *   rect(25, 25, 50, 50);
     *
     *   rectMode(CORNERS);
     *   fill(100);
     *   rect(25, 25, 50, 50);
     *
     *   describe('A small gray square drawn at the top-left corner of a white square.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   rectMode(RADIUS);
     *   fill(255);
     *   rect(50, 50, 30, 30);
     *
     *   rectMode(CENTER);
     *   fill(100);
     *   rect(50, 50, 30, 30);
     *
     *   describe('A small gray square drawn at the center of a white square.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   rectMode(CORNER);
     *   fill(255);
     *   square(25, 25, 50);
     *
     *   describe('A white square.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   rectMode(RADIUS);
     *   fill(255);
     *   square(50, 50, 30);
     *
     *   rectMode(CENTER);
     *   fill(100);
     *   square(50, 50, 30);
     *
     *   describe('A small gray square drawn at the center of a white square.');
     * }
     */
    fn.rectMode = function(m) {
      // p5._validateParameters('rectMode', arguments);
      if (
        m === CORNER ||
        m === CORNERS ||
        m === RADIUS ||
        m === CENTER
      ) {
        this._renderer.states.setValue('rectMode', m);
      }
      return this; // return current rectMode ?
    };

    /**
     * Draws certain features with smooth (antialiased) edges.
     *
     * `smooth()` is active by default. In 2D mode,
     * <a href="#/p5/noSmooth">noSmooth()</a> is helpful for scaling up images
     * without blurring. The functions don't affect shapes or fonts.
     *
     * In WebGL mode, <a href="#/p5/noSmooth">noSmooth()</a> causes all shapes to
     * be drawn with jagged (aliased) edges. The functions don't affect images or
     * fonts.
     *
     * @method smooth
     * @chainable
     *
     * @example
     * let heart;
     *
     * async function setup() {
     *   // Load a pixelated heart image from an image data string.
     *   heart = await loadImage('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAAXNSR0IArs4c6QAAAEZJREFUGFd9jcsNACAIQ9tB2MeR3YdBMBBq8CIXPi2vBICIiOwkOedatllqWO6Y8yOWoyuNf1GZwgmf+RRG2YXr+xVFmA8HZ9Mx/KGPMtcAAAAASUVORK5CYII=');
     *
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Antialiased hearts.
     *   image(heart, 10, 10);
     *   image(heart, 20, 10, 16, 16);
     *   image(heart, 40, 10, 32, 32);
     *
     *   // Aliased hearts.
     *   noSmooth();
     *   image(heart, 10, 60);
     *   image(heart, 20, 60, 16, 16);
     *   image(heart, 40, 60, 32, 32);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   circle(0, 0, 80);
     *
     *   describe('A white circle on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Disable smoothing.
     *   noSmooth();
     *
     *   background(200);
     *
     *   circle(0, 0, 80);
     *
     *   describe('A pixelated white circle on a gray background.');
     * }
     */
    fn.smooth = function() {
      if (!this._renderer.isP3D) {
        if ('imageSmoothingEnabled' in this.drawingContext) {
          this.drawingContext.imageSmoothingEnabled = true;
        }
      } else {
        this.setAttributes('antialias', true);
      }
      return this;
    };

    /**
     * Sets the style for rendering the ends of lines.
     *
     * The caps for line endings are either rounded (`ROUND`), squared
     * (`SQUARE`), or extended (`PROJECT`). The default cap is `ROUND`.
     *
     * The argument passed to `strokeCap()` must be written in ALL CAPS because
     * the constants `ROUND`, `SQUARE`, and `PROJECT` are defined this way.
     * JavaScript is a case-sensitive language.
     *
     * @method strokeCap
     * @param  {(ROUND|SQUARE|PROJECT)} cap either ROUND, SQUARE, or PROJECT
     * @chainable
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   strokeWeight(12);
     *
     *   // Top.
     *   strokeCap(ROUND);
     *   line(20, 30, 80, 30);
     *
     *   // Middle.
     *   strokeCap(SQUARE);
     *   line(20, 50, 80, 50);
     *
     *   // Bottom.
     *   strokeCap(PROJECT);
     *   line(20, 70, 80, 70);
     *
     *   describe(
     *     'Three horizontal lines. The top line has rounded ends, the middle line has squared ends, and the bottom line has longer, squared ends.'
     *   );
     * }
     */
    fn.strokeCap = function(cap) {
      // p5._validateParameters('strokeCap', arguments);
      if (
        cap === ROUND ||
        cap === SQUARE ||
        cap === PROJECT
      ) {
        this._renderer.strokeCap(cap);
      }
      return this;
    };

    /**
     * Sets the style of the joints that connect line segments.
     *
     * Joints are either mitered (`MITER`), beveled (`BEVEL`), or rounded
     * (`ROUND`). The default joint is `MITER` in 2D mode and `ROUND` in WebGL
     * mode.
     *
     * The argument passed to `strokeJoin()` must be written in ALL CAPS because
     * the constants `MITER`, `BEVEL`, and `ROUND` are defined this way.
     * JavaScript is a case-sensitive language.
     *
     * @method strokeJoin
     * @param  {(MITER|BEVEL|ROUND)} join either MITER, BEVEL, or ROUND
     * @chainable
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the line.
     *   noFill();
     *   strokeWeight(10);
     *   strokeJoin(MITER);
     *
     *   // Draw the line.
     *   beginShape();
     *   vertex(35, 20);
     *   vertex(65, 50);
     *   vertex(35, 80);
     *   endShape();
     *
     *   describe('A right-facing arrowhead shape with a pointed tip in center of canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the line.
     *   noFill();
     *   strokeWeight(10);
     *   strokeJoin(BEVEL);
     *
     *   // Draw the line.
     *   beginShape();
     *   vertex(35, 20);
     *   vertex(65, 50);
     *   vertex(35, 80);
     *   endShape();
     *
     *   describe('A right-facing arrowhead shape with a flat tip in center of canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the line.
     *   noFill();
     *   strokeWeight(10);
     *   strokeJoin(ROUND);
     *
     *   // Draw the line.
     *   beginShape();
     *   vertex(35, 20);
     *   vertex(65, 50);
     *   vertex(35, 80);
     *   endShape();
     *
     *   describe('A right-facing arrowhead shape with a rounded tip in center of canvas.');
     * }
     */
    fn.strokeJoin = function(join) {
      // p5._validateParameters('strokeJoin', arguments);
      if (
        join === ROUND ||
        join === BEVEL ||
        join === MITER
      ) {
        this._renderer.strokeJoin(join);
      }
      return this;
    };

    /**
     * Sets the width of the stroke used for points, lines, and the outlines of
     * shapes.
     *
     * Note: `strokeWeight()` is affected by transformations, especially calls to
     * <a href="#/p5/scale">scale()</a>.
     *
     * @method strokeWeight
     * @param  {Number} weight the weight of the stroke (in pixels).
     * @chainable
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Top.
     *   line(20, 20, 80, 20);
     *
     *   // Middle.
     *   strokeWeight(4);
     *   line(20, 40, 80, 40);
     *
     *   // Bottom.
     *   strokeWeight(10);
     *   line(20, 70, 80, 70);
     *
     *   describe('Three horizontal black lines. The top line is thin, the middle is medium, and the bottom is thick.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Top.
     *   line(20, 20, 80, 20);
     *
     *   // Scale by a factor of 5.
     *   scale(5);
     *
     *   // Bottom. Coordinates are adjusted for scaling.
     *   line(4, 8, 16, 8);
     *
     *   describe('Two horizontal black lines. The top line is thin and the bottom is five times thicker than the top.');
     * }
     */
    fn.strokeWeight = function(w) {
      // p5._validateParameters('strokeWeight', arguments);
      this._renderer.strokeWeight(w);
      return this;
    };
  }

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

  /**
   * @module Shape
   * @submodule Curves
   * @for p5
   * @requires core
   */

  function curves(p5, fn){
    /**
     * Draws a Bézier curve.
     *
     * Bézier curves can form shapes and curves that slope gently. They're defined
     * by two anchor points and two control points. Bézier curves provide more
     * control than the spline curves created with the
     * <a href="#/p5/spline">spline()</a> function.
     *
     * The first two parameters, `x1` and `y1`, set the first anchor point. The
     * first anchor point is where the curve starts.
     *
     * The next four parameters, `x2`, `y2`, `x3`, and `y3`, set the two control
     * points. The control points "pull" the curve towards them.
     *
     * The seventh and eighth parameters, `x4` and `y4`, set the last anchor
     * point. The last anchor point is where the curve ends.
     *
     * Bézier curves can also be drawn in 3D using WebGL mode. The 3D version of
     * `bezier()` has twelve arguments because each point has x-, y-,
     * and z-coordinates.
     *
     * @method bezier
     * @param  {Number} x1 x-coordinate of the first anchor point.
     * @param  {Number} y1 y-coordinate of the first anchor point.
     * @param  {Number} x2 x-coordinate of the first control point.
     * @param  {Number} y2 y-coordinate of the first control point.
     * @param  {Number} x3 x-coordinate of the second control point.
     * @param  {Number} y3 y-coordinate of the second control point.
     * @param  {Number} x4 x-coordinate of the second anchor point.
     * @param  {Number} y4 y-coordinate of the second anchor point.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Draw the anchor points in black.
     *   stroke(0);
     *   strokeWeight(5);
     *   point(85, 20);
     *   point(15, 80);
     *
     *   // Draw the control points in red.
     *   stroke(255, 0, 0);
     *   point(10, 10);
     *   point(90, 90);
     *
     *   // Draw a black bezier curve.
     *   noFill();
     *   stroke(0);
     *   strokeWeight(1);
     *   bezier(85, 20, 10, 10, 90, 90, 15, 80);
     *
     *   // Draw red lines from the anchor points to the control points.
     *   stroke(255, 0, 0);
     *   line(85, 20, 10, 10);
     *   line(15, 80, 90, 90);
     *
     *   describe(
     *     'A gray square with three curves. A black s-curve has two straight, red lines that extend from its ends. The endpoints of all the curves are marked with dots.'
     *   );
     * }
     *
     * @example
     * // Click the mouse near the red dot in the top-left corner
     * // and drag to change the curve's shape.
     *
     * let x2 = 10;
     * let y2 = 10;
     * let isChanging = false;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with three curves. A black s-curve has two straight, red lines that extend from its ends. The endpoints of all the curves are marked with dots.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw the anchor points in black.
     *   stroke(0);
     *   strokeWeight(5);
     *   point(85, 20);
     *   point(15, 80);
     *
     *   // Draw the control points in red.
     *   stroke(255, 0, 0);
     *   point(x2, y2);
     *   point(90, 90);
     *
     *   // Draw a black bezier curve.
     *   noFill();
     *   stroke(0);
     *   strokeWeight(1);
     *   bezier(85, 20, x2, y2, 90, 90, 15, 80);
     *
     *   // Draw red lines from the anchor points to the control points.
     *   stroke(255, 0, 0);
     *   line(85, 20, x2, y2);
     *   line(15, 80, 90, 90);
     * }
     *
     * // Start changing the first control point if the user clicks near it.
     * function mousePressed() {
     *   if (dist(mouseX, mouseY, x2, y2) < 20) {
     *     isChanging = true;
     *   }
     * }
     *
     * // Stop changing the first control point when the user releases the mouse.
     * function mouseReleased() {
     *   isChanging = false;
     * }
     *
     * // Update the first control point while the user drags the mouse.
     * function mouseDragged() {
     *   if (isChanging === true) {
     *     x2 = mouseX;
     *     y2 = mouseY;
     *   }
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background('skyblue');
     *
     *   // Draw the red balloon.
     *   fill('red');
     *   bezier(50, 60, 5, 15, 95, 15, 50, 60);
     *
     *   // Draw the balloon string.
     *   line(50, 60, 50, 80);
     *
     *   describe('A red balloon in a blue sky.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A red balloon in a blue sky. The balloon rotates slowly, revealing that it is flat.');
     * }
     *
     * function draw() {
     *   background('skyblue');
     *
     *   // Rotate around the y-axis.
     *   rotateY(frameCount * 0.01);
     *
     *   // Draw the red balloon.
     *   fill('red');
     *   bezier(0, 0, 0, -45, -45, 0, 45, -45, 0, 0, 0, 0);
     *
     *   // Draw the balloon string.
     *   line(0, 0, 0, 0, 20, 0);
     * }
     */
    /**
     * @method bezier
     * @param  {Number} x1
     * @param  {Number} y1
     * @param  {Number} z1 z-coordinate of the first anchor point.
     * @param  {Number} x2
     * @param  {Number} y2
     * @param  {Number} z2 z-coordinate of the first control point.
     * @param  {Number} x3
     * @param  {Number} y3
     * @param  {Number} z3 z-coordinate of the second control point.
     * @param  {Number} x4
     * @param  {Number} y4
     * @param  {Number} z4 z-coordinate of the second anchor point.
     * @chainable
     */
    fn.bezier = function(...args) {
      // p5._validateParameters('bezier', args);

      // if the current stroke and fill settings wouldn't result in something
      // visible, exit immediately
      if (
        !this._renderer.states.strokeColor &&
        !this._renderer.states.fillColor
      ) {
        return this;
      }

      this._renderer.bezier(...args);

      return this;
    };

    /**
     * Calculates coordinates along a Bézier curve using interpolation.
     *
     * `bezierPoint()` calculates coordinates along a Bézier curve using the
     * anchor and control points. It expects points in the same order as the
     * <a href="#/p5/bezier">bezier()</a> function. `bezierPoint()` works one axis
     * at a time. Passing the anchor and control points' x-coordinates will
     * calculate the x-coordinate of a point on the curve. Passing the anchor and
     * control points' y-coordinates will calculate the y-coordinate of a point on
     * the curve.
     *
     * The first parameter, `a`, is the coordinate of the first anchor point.
     *
     * The second and third parameters, `b` and `c`, are the coordinates of the
     * control points.
     *
     * The fourth parameter, `d`, is the coordinate of the last anchor point.
     *
     * The fifth parameter, `t`, is the amount to interpolate along the curve. 0
     * is the first anchor point, 1 is the second anchor point, and 0.5 is halfway
     * between them.
     *
     * @method bezierPoint
     * @param {Number} a coordinate of first anchor point.
     * @param {Number} b coordinate of first control point.
     * @param {Number} c coordinate of second control point.
     * @param {Number} d coordinate of second anchor point.
     * @param {Number} t amount to interpolate between 0 and 1.
     * @return {Number} coordinate of the point on the curve.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the coordinates for the curve's anchor and control points.
     *   let x1 = 85;
     *   let x2 = 10;
     *   let x3 = 90;
     *   let x4 = 15;
     *   let y1 = 20;
     *   let y2 = 10;
     *   let y3 = 90;
     *   let y4 = 80;
     *
     *   // Style the curve.
     *   noFill();
     *
     *   // Draw the curve.
     *   bezier(x1, y1, x2, y2, x3, y3, x4, y4);
     *
     *   // Draw circles along the curve's path.
     *   fill(255);
     *
     *   // Top-right.
     *   let x = bezierPoint(x1, x2, x3, x4, 0);
     *   let y = bezierPoint(y1, y2, y3, y4, 0);
     *   circle(x, y, 5);
     *
     *   // Center.
     *   x = bezierPoint(x1, x2, x3, x4, 0.5);
     *   y = bezierPoint(y1, y2, y3, y4, 0.5);
     *   circle(x, y, 5);
     *
     *   // Bottom-left.
     *   x = bezierPoint(x1, x2, x3, x4, 1);
     *   y = bezierPoint(y1, y2, y3, y4, 1);
     *   circle(x, y, 5);
     *
     *   describe('A black s-curve on a gray square. The endpoints and center of the curve are marked with white circles.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A black s-curve on a gray square. A white circle moves back and forth along the curve.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Set the coordinates for the curve's anchor and control points.
     *   let x1 = 85;
     *   let x2 = 10;
     *   let x3 = 90;
     *   let x4 = 15;
     *   let y1 = 20;
     *   let y2 = 10;
     *   let y3 = 90;
     *   let y4 = 80;
     *
     *   // Draw the curve.
     *   noFill();
     *   bezier(x1, y1, x2, y2, x3, y3, x4, y4);
     *
     *   // Calculate the circle's coordinates.
     *   let t = 0.5 * sin(frameCount * 0.01) + 0.5;
     *   let x = bezierPoint(x1, x2, x3, x4, t);
     *   let y = bezierPoint(y1, y2, y3, y4, t);
     *
     *   // Draw the circle.
     *   fill(255);
     *   circle(x, y, 5);
     * }
     */
    fn.bezierPoint = function(a, b, c, d, t) {
      // p5._validateParameters('bezierPoint', arguments);

      const adjustedT = 1 - t;
      return (
        Math.pow(adjustedT, 3) * a +
        3 * Math.pow(adjustedT, 2) * t * b +
        3 * adjustedT * Math.pow(t, 2) * c +
        Math.pow(t, 3) * d
      );
    };

    /**
     * Calculates coordinates along a line that's tangent to a Bézier curve.
     *
     * Tangent lines skim the surface of a curve. A tangent line's slope equals
     * the curve's slope at the point where it intersects.
     *
     * `bezierTangent()` calculates coordinates along a tangent line using the
     * Bézier curve's anchor and control points. It expects points in the same
     * order as the <a href="#/p5/bezier">bezier()</a> function. `bezierTangent()`
     * works one axis at a time. Passing the anchor and control points'
     * x-coordinates will calculate the x-coordinate of a point on the tangent
     * line. Passing the anchor and control points' y-coordinates will calculate
     * the y-coordinate of a point on the tangent line.
     *
     * The first parameter, `a`, is the coordinate of the first anchor point.
     *
     * The second and third parameters, `b` and `c`, are the coordinates of the
     * control points.
     *
     * The fourth parameter, `d`, is the coordinate of the last anchor point.
     *
     * The fifth parameter, `t`, is the amount to interpolate along the curve. 0
     * is the first anchor point, 1 is the second anchor point, and 0.5 is halfway
     * between them.
     *
     * @method bezierTangent
     * @param {Number} a coordinate of first anchor point.
     * @param {Number} b coordinate of first control point.
     * @param {Number} c coordinate of second control point.
     * @param {Number} d coordinate of second anchor point.
     * @param {Number} t amount to interpolate between 0 and 1.
     * @return {Number} coordinate of a point on the tangent line.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the coordinates for the curve's anchor and control points.
     *   let x1 = 85;
     *   let x2 = 10;
     *   let x3 = 90;
     *   let x4 = 15;
     *   let y1 = 20;
     *   let y2 = 10;
     *   let y3 = 90;
     *   let y4 = 80;
     *
     *   // Style the curve.
     *   noFill();
     *
     *   // Draw the curve.
     *   bezier(x1, y1, x2, y2, x3, y3, x4, y4);
     *
     *   // Draw tangents along the curve's path.
     *   fill(255);
     *
     *   // Top-right circle.
     *   stroke(0);
     *   let x = bezierPoint(x1, x2, x3, x4, 0);
     *   let y = bezierPoint(y1, y2, y3, y4, 0);
     *   circle(x, y, 5);
     *
     *   // Top-right tangent line.
     *   // Scale the tangent point to draw a shorter line.
     *   stroke(255, 0, 0);
     *   let tx = 0.1 * bezierTangent(x1, x2, x3, x4, 0);
     *   let ty = 0.1 * bezierTangent(y1, y2, y3, y4, 0);
     *   line(x + tx, y + ty, x - tx, y - ty);
     *
     *   // Center circle.
     *   stroke(0);
     *   x = bezierPoint(x1, x2, x3, x4, 0.5);
     *   y = bezierPoint(y1, y2, y3, y4, 0.5);
     *   circle(x, y, 5);
     *
     *   // Center tangent line.
     *   // Scale the tangent point to draw a shorter line.
     *   stroke(255, 0, 0);
     *   tx = 0.1 * bezierTangent(x1, x2, x3, x4, 0.5);
     *   ty = 0.1 * bezierTangent(y1, y2, y3, y4, 0.5);
     *   line(x + tx, y + ty, x - tx, y - ty);
     *
     *   // Bottom-left circle.
     *   stroke(0);
     *   x = bezierPoint(x1, x2, x3, x4, 1);
     *   y = bezierPoint(y1, y2, y3, y4, 1);
     *   circle(x, y, 5);
     *
     *   // Bottom-left tangent.
     *   // Scale the tangent point to draw a shorter line.
     *   stroke(255, 0, 0);
     *   tx = 0.1 * bezierTangent(x1, x2, x3, x4, 1);
     *   ty = 0.1 * bezierTangent(y1, y2, y3, y4, 1);
     *   line(x + tx, y + ty, x - tx, y - ty);
     *
     *   describe(
     *     'A black s-curve on a gray square. The endpoints and center of the curve are marked with white circles. Red tangent lines extend from the white circles.'
     *   );
     * }
     */
    fn.bezierTangent = function(a, b, c, d, t) {
      // p5._validateParameters('bezierTangent', arguments);

      const adjustedT = 1 - t;
      return (
        3 * d * Math.pow(t, 2) -
        3 * c * Math.pow(t, 2) +
        6 * c * adjustedT * t -
        6 * b * adjustedT * t +
        3 * b * Math.pow(adjustedT, 2) -
        3 * a * Math.pow(adjustedT, 2)
      );
    };

    /**
     * Draws a curve using a Catmull-Rom spline.
     *
     * Spline curves can form shapes and curves that slope gently. They’re like
     * cables that are attached to a set of points. By default (`ends: INCLUDE`),
     * the curve passes through all four points you provide, in order
     * `p0(x1,y1)` -> `p1(x2,y2)` -> `p2(x3,y3)` -> `p3(x4,y4)`. Think of them as
     * points on a curve. If you switch to `ends: EXCLUDE`, p0 and p3 act
     * like control points and only the middle span `p1->p2` is drawn.
     *
     * Spline curves can also be drawn in 3D using WebGL mode. The 3D version of
     * `spline()` has twelve arguments because each point has x-, y-, and
     * z-coordinates.
     *
     * @method spline
     * @param  {Number} x1 x-coordinate of point p0.
     * @param  {Number} y1 y-coordinate of point p0.
     * @param  {Number} x2 x-coordinate of point p1.
     * @param  {Number} y2 y-coordinate of point p1.
     * @param  {Number} x3 x-coordinate of point p2.
     * @param  {Number} y3 y-coordinate of point p2.
     * @param  {Number} x4 x-coordinate of point p3.
     * @param  {Number} y4 y-coordinate of point p3.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(200, 200);
     *   background(240);
     *   noFill();
     *
     *   stroke(0);
     *   strokeWeight(2);
     *   spline(40, 60, 100, 40, 120, 120, 60, 140);
     *
     *   strokeWeight(5);
     *   point(40, 60);
     *   point(100, 40);
     *   point(120, 120);
     *   point(60, 140);
     *
     *   describe('A black spline passes smoothly through four points');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(200, 200);
     *   background(245);
     *
     *   // Ensure the curve includes both end spans p0->p1 and p2->p3
     *   splineProperty('ends', INCLUDE);
     *
     *   // Control / anchor points
     *   const p0 = createVector(30, 160);
     *   const p1 = createVector(60, 40);
     *   const p2 = createVector(140, 40);
     *   const p3 = createVector(170, 160);
     *
     *   // Draw the spline that passes through ALL four points (INCLUDE)
     *   noFill();
     *   stroke(0);
     *   strokeWeight(2);
     *   spline(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
     *
     *   // Draw markers + labels
     *   fill(255);
     *   stroke(0);
     *   const r = 6;
     *   circle(p0.x, p0.y, r);
     *   circle(p1.x, p1.y, r);
     *   circle(p2.x, p2.y, r);
     *   circle(p3.x, p3.y, r);
     *
     *   noStroke();
     *   fill(0);
     *   text('p0', p0.x - 14, p0.y + 14);
     *   text('p1', p1.x - 14, p1.y - 8);
     *   text('p2', p2.x + 4, p2.y - 8);
     *   text('p3', p3.x + 4, p3.y + 14);
     *
     *   describe('A black Catmull-Rom spline passes through p0, p1, p2, p3 with endpoints included.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Exclude the ends—skip the outer spans (p0→p1 and p2→p3) so only the middle span (p1→p2) is drawn.
     *   splineProperty('ends', EXCLUDE);
     *
     *   // Draw a black spline curve.
     *   noFill();
     *   strokeWeight(1);
     *   stroke(0);
     *   spline(5, 26, 73, 24, 73, 61, 15, 65);
     *
     *   // Draw red spline curves from the points.
     *   stroke(255, 0, 0);
     *   spline(5, 26, 5, 26, 73, 24, 73, 61);
     *   spline(73, 24, 73, 61, 15, 65, 15, 65);
     *
     *   // Draw the points in black.
     *   strokeWeight(5);
     *   stroke(0);
     *   point(73, 24);
     *   point(73, 61);
     *
     *   // Draw the points in red.
     *   stroke(255, 0, 0);
     *   point(5, 26);
     *   point(15, 65);
     *
     *   describe(
     *     'A gray square with a curve drawn in three segments. The curve is a sideways U shape with red segments on top and bottom, and a black segment on the right. The endpoints of all the segments are marked with dots.'
     *   );
     * }
     *
     * @example
     * let x1 = 5;
     * let y1 = 26;
     * let isChanging = false;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a curve drawn in three segments. The curve is a sideways U shape with red segments on top and bottom, and a black segment on the right. The endpoints of all the segments are marked with dots.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Exclude the ends—skip the outer spans (p0→p1 and p2→p3) so only the middle span (p1→p2) is drawn.
     *   splineProperty('ends', EXCLUDE);
     *
     *   // Draw a black spline curve.
     *   noFill();
     *   strokeWeight(1);
     *   stroke(0);
     *   spline(x1, y1, 73, 24, 73, 61, 15, 65);
     *
     *   // Draw red spline curves from the points.
     *   stroke(255, 0, 0);
     *   spline(x1, y1, x1, y1, 73, 24, 73, 61);
     *   spline(73, 24, 73, 61, 15, 65, 15, 65);
     *
     *   // Draw the anchor points in black.
     *   strokeWeight(5);
     *   stroke(0);
     *   point(73, 24);
     *   point(73, 61);
     *
     *   // Draw the points in red.
     *   stroke(255, 0, 0);
     *   point(x1, y1);
     *   point(15, 65);
     * }
     *
     * // Start changing the first point if the user clicks near it.
     * function mousePressed() {
     *   if (dist(mouseX, mouseY, x1, y1) < 20) {
     *     isChanging = true;
     *   }
     * }
     *
     * // Stop changing the first point when the user releases the mouse.
     * function mouseReleased() {
     *   isChanging = false;
     * }
     *
     * // Update the first point while the user drags the mouse.
     * function mouseDragged() {
     *   if (isChanging === true) {
     *     x1 = mouseX;
     *     y1 = mouseY;
     *   }
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background('skyblue');
     *
     *   // Exclude the ends—skip the outer spans (p0→p1 and p2→p3) so only the middle span (p1→p2) is drawn.
     *   splineProperty('ends', EXCLUDE);
     *
     *   // Draw the red balloon.
     *   fill('red');
     *   spline(-150, 275, 50, 60, 50, 60, 250, 275);
     *
     *   // Draw the balloon string.
     *   line(50, 60, 50, 80);
     *
     *   describe('A red balloon in a blue sky.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A red balloon in a blue sky.');
     * }
     *
     * function draw() {
     *   background('skyblue');
     *
     *   // Exclude the ends—skip the outer spans (p0→p1 and p2→p3) so only the middle span (p1→p2) is drawn.
     *   splineProperty('ends', EXCLUDE);
     *
     *   // Rotate around the y-axis.
     *   rotateY(frameCount * 0.01);
     *
     *   // Draw the red balloon.
     *   fill('red');
     *   spline(-200, 225, 0, 0, 10, 0, 0, 10, 0, 200, 225, 0);
     *
     *   // Draw the balloon string.
     *   line(0, 10, 0, 0, 30, 0);
     * }
     */
    /**
     * @method spline
     * @param  {Number} x1
     * @param  {Number} y1
     * @param  {Number} z1 z-coordinate of point p0.
     * @param  {Number} x2
     * @param  {Number} y2
     * @param  {Number} z2 z-coordinate of point p1.
     * @param  {Number} x3
     * @param  {Number} y3
     * @param  {Number} z3 z-coordinate of point p2.
     * @param  {Number} x4
     * @param  {Number} y4
     * @param  {Number} z4 z-coordinate of point p3.
     * @chainable
     */
    fn.spline = function(...args) {
      if (
        !this._renderer.states.strokeColor &&
        !this._renderer.states.fillColor
      ) {
        return this;
      }
      this._renderer.spline(...args);

      return this;
    };

    /**
     * Calculates coordinates along a spline curve using interpolation.
     *
     * `splinePoint()` calculates coordinates along a spline curve using four
     * points p0, p1, p2, p3. It expects points in the same order as the
     * <a href="#/p5/spline">spline()</a> function. `splinePoint()` works one axis
     * at a time. Passing the points' x-coordinates will
     * calculate the x-coordinate of a point on the curve. Passing the
     * points' y-coordinates will calculate the y-coordinate of a point on
     * the curve.
     *
     * The first parameter, `a`, is the coordinate of point p0.
     *
     * The second and third parameters, `b` and `c`, are the coordinates of
     * points p1 and p2.
     *
     * The fourth parameter, `d`, is the coordinate of point p3.
     *
     * The fifth parameter, `t`, is the amount to interpolate along the span
     * from p1 to p2. `t = 0` is p1, `t = 1` is p2, and `t = 0.5` is halfway
     * between them.
     *
     * @method splinePoint
     * @param {Number} a coordinate of point p0.
     * @param {Number} b coordinate of point p1.
     * @param {Number} c coordinate of point p2.
     * @param {Number} d coordinate of point p3.
     * @param {Number} t amount to interpolate between 0 and 1.
     * @return {Number} coordinate of a point on the curve.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *
     *   // Set the coordinates for the curve's four points (p0, p1, p2, p3).
     *   let x1 = 5;
     *   let y1 = 26;
     *   let x2 = 73;
     *   let y2 = 24;
     *   let x3 = 73;
     *   let y3 = 61;
     *   let x4 = 15;
     *   let y4 = 65;
     *
     *   // Draw the curve.
     *   noFill();
     *   spline(x1, y1, x2, y2, x3, y3, x4, y4);
     *
     *   // Draw circles along the curve's path.
     *   fill(255);
     *
     *   // Top.
     *   let x = splinePoint(x1, x2, x3, x4, 0);
     *   let y = splinePoint(y1, y2, y3, y4, 0);
     *   circle(x, y, 5);
     *
     *   // Center.
     *   x = splinePoint(x1, x2, x3, x4, 0.5);
     *   y = splinePoint(y1, y2, y3, y4, 0.5);
     *   circle(x, y, 5);
     *
     *   // Bottom.
     *   x = splinePoint(x1, x2, x3, x4, 1);
     *   y = splinePoint(y1, y2, y3, y4, 1);
     *   circle(x, y, 5);
     *
     *   describe('A black curve on a gray square. The endpoints and center of the curve are marked with white circles.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A black curve on a gray square. A white circle moves back and forth along the curve.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Set the coordinates for the curve's four points (p0, p1, p2, p3).
     *   let x1 = 5;
     *   let y1 = 26;
     *   let x2 = 73;
     *   let y2 = 24;
     *   let x3 = 73;
     *   let y3 = 61;
     *   let x4 = 15;
     *   let y4 = 65;
     *
     *   // Draw the curve.
     *   noFill();
     *   spline(x1, y1, x2, y2, x3, y3, x4, y4);
     *
     *   // Calculate the circle's coordinates.
     *   let t = 0.5 * sin(frameCount * 0.01) + 0.5;
     *   let x = splinePoint(x1, x2, x3, x4, t);
     *   let y = splinePoint(y1, y2, y3, y4, t);
     *
     *   // Draw the circle.
     *   fill(255);
     *   circle(x, y, 5);
     * }
     *
     * @example
     * let p0, p1, p2, p3;
     *
     * function setup() {
     *   createCanvas(200, 200);
     *   splineProperty('ends', INCLUDE); // make endpoints part of the curve
     *
     *   // Four points forming a gentle arch
     *   p0 = createVector(30, 160);
     *   p1 = createVector(60, 50);
     *   p2 = createVector(140, 50);
     *   p3 = createVector(170, 160);
     *
     *   describe('Black spline through p0–p3. A red dot marks the location at parameter t on p1->p2 using splinePoint.');
     * }
     *
     * function draw() {
     *   background(245);
     *
     *   // Draw the spline for context
     *   noFill();
     *   stroke(0);
     *   strokeWeight(2);
     *   spline(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
     *
     *   // Map mouse X to t in [0, 1] (span p1->p2)
     *   let t = constrain(map(mouseX, 0, width, 0, 1), 0, 1);
     *
     *   // Evaluate the curve point by axis (splinePoint works one axis at a time)
     *   let x = splinePoint(p0.x, p1.x, p2.x, p3.x, t);
     *   let y = splinePoint(p0.y, p1.y, p2.y, p3.y, t);
     *
     *   // Marker at the evaluated position
     *   noStroke();
     *   fill('red');
     *   circle(x, y, 8);
     *
     *   // Draw control/anchor points
     *   stroke(0);
     *   strokeWeight(1);
     *   fill(255);
     *   const r = 6;
     *   circle(p0.x, p0.y, r);
     *   circle(p1.x, p1.y, r);
     *   circle(p2.x, p2.y, r);
     *   circle(p3.x, p3.y, r);
     *
     *   // Labels + UI hint
     *   noStroke();
     *   fill(20);
     *   textSize(10);
     *   text('p0', p0.x - 12, p0.y + 14);
     *   text('p1', p1.x - 12, p1.y - 8);
     *   text('p2', p2.x + 4, p2.y - 8);
     *   text('p3', p3.x + 4, p3.y + 14);
     *   text('t = ' + nf(t, 1, 2) + ' (p1→p2)', 8, 16);
     * }
     */
    fn.splinePoint = function(a, b, c, d, t) {
      const s = this._renderer.states.splineProperties.tightness,
        t3 = t * t * t,
        t2 = t * t,
        f1 = (s - 1) / 2 * t3 + (1 - s) * t2 + (s - 1) / 2 * t,
        f2 = (s + 3) / 2 * t3 + (-5 - s) / 2 * t2 + 1.0,
        f3 = (-3 - s) / 2 * t3 + (s + 2) * t2 + (1 - s) / 2 * t,
        f4 = (1 - s) / 2 * t3 + (s - 1) / 2 * t2;
      return a * f1 + b * f2 + c * f3 + d * f4;
    };

    /**
     * Calculates coordinates along a line that's tangent to a spline curve.
     *
     * Tangent lines skim the surface of a curve. A tangent line's slope equals
     * the curve's slope at the point where it intersects.
     *
     * `splineTangent()` calculates coordinates along a tangent line using four
     * points p0, p1, p2, p3. It expects points in the same order as the
     * <a href="#/p5/spline">spline()</a> function. `splineTangent()` works one
     * axis at a time. Passing the points' x-coordinates returns the x-component of
     * the tangent vector; passing the points' y-coordinates returns the y-component.
     * The first parameter, `a`, is the coordinate of point p0.
     *
     * The second and third parameters, `b` and `c`, are the coordinates of
     * points p1 and p2.
     *
     * The fourth parameter, `d`, is the coordinate of point p3.
     *
     * The fifth parameter, `t`, is the amount to interpolate along the span
     * from p1 to p2. `t = 0` is p1, `t = 1` is p2, and `t = 0.5` is halfway
     * between them.
     *
     * @method splineTangent
     * @param {Number} a coordinate of point p0.
     * @param {Number} b coordinate of point p1.
     * @param {Number} c coordinate of point p2.
     * @param {Number} d coordinate of point p3.
     * @param {Number} t amount to interpolate between 0 and 1.
     * @return {Number} coordinate of a point on the tangent line.
     *
     * @example
     * function setup() {
     *   createCanvas(120, 120);
     *   describe('A black spline on a gray canvas. A red dot moves along the curve on its own. A short line shows the tangent direction at the dot.');
     * }
     *
     * function draw() {
     *   background(240);
     *
     *   const x1 = 15, y1 = 40;
     *   const x2 = 90, y2 = 25;
     *   const x3 = 95, y3 = 95;
     *   const x4 = 30, y4 = 110;
     *
     *   noFill();
     *   stroke(0);
     *   strokeWeight(2);
     *   spline(x1, y1, x2, y2, x3, y3, x4, y4);
     *
     *   const t = 0.5 + 0.5 * sin(frameCount * 0.03);
     *
     *   const px = splinePoint(x1, x2, x3, x4, t);
     *   const py = splinePoint(y1, y2, y3, y4, t);
     *
     *   let tx = splineTangent(x1, x2, x3, x4, t);
     *   let ty = splineTangent(y1, y2, y3, y4, t);
     *
     *   const m = Math.hypot(tx, ty) || 1;
     *   tx = (tx / m) * 16;
     *   ty = (ty / m) * 16;
     *
     *   stroke(0);
     *   strokeWeight(2);
     *   line(px, py, px + tx, py + ty);
     *
     *   noStroke();
     *   fill('red');
     *   circle(px, py, 7);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the coordinates for the curve's four points (p0, p1, p2, p3).
     *   let x1 = 5;
     *   let y1 = 26;
     *   let x2 = 73;
     *   let y2 = 24;
     *   let x3 = 73;
     *   let y3 = 61;
     *   let x4 = 15;
     *   let y4 = 65;
     *
     *   // Draw the curve.
     *   noFill();
     *   spline(x1, y1, x2, y2, x3, y3, x4, y4);
     *
     *   // Draw tangents along the curve's path.
     *   fill(255);
     *
     *   // Top circle.
     *   stroke(0);
     *   let x = splinePoint(x1, x2, x3, x4, 0);
     *   let y = splinePoint(y1, y2, y3, y4, 0);
     *   circle(x, y, 5);
     *
     *   // Top tangent line.
     *   // Scale the tangent point to draw a shorter line.
     *   stroke(255, 0, 0);
     *   let tx = 0.2 * splineTangent(x1, x2, x3, x4, 0);
     *   let ty = 0.2 * splineTangent(y1, y2, y3, y4, 0);
     *   line(x + tx, y + ty, x - tx, y - ty);
     *
     *   // Center circle.
     *   stroke(0);
     *   x = splinePoint(x1, x2, x3, x4, 0.5);
     *   y = splinePoint(y1, y2, y3, y4, 0.5);
     *   circle(x, y, 5);
     *
     *   // Center tangent line.
     *   // Scale the tangent point to draw a shorter line.
     *   stroke(255, 0, 0);
     *   tx = 0.2 * splineTangent(x1, x2, x3, x4, 0.5);
     *   ty = 0.2 * splineTangent(y1, y2, y3, y4, 0.5);
     *   line(x + tx, y + ty, x - tx, y - ty);
     *
     *   // Bottom circle.
     *   stroke(0);
     *   x = splinePoint(x1, x2, x3, x4, 1);
     *   y = splinePoint(y1, y2, y3, y4, 1);
     *   circle(x, y, 5);
     *
     *   // Bottom tangent line.
     *   // Scale the tangent point to draw a shorter line.
     *   stroke(255, 0, 0);
     *   tx = 0.2 * splineTangent(x1, x2, x3, x4, 1);
     *   ty = 0.2 * splineTangent(y1, y2, y3, y4, 1);
     *   line(x + tx, y + ty, x - tx, y - ty);
     *
     *   describe(
     *     'A black curve on a gray square. A white circle moves back and forth along the curve.'
     *   );
     * }
     */
    fn.splineTangent = function(a, b, c, d, t) {
      const s = this._renderer.states.splineProperties.tightness,
        tt3 = t * t * 3,
        t2 = t * 2,
        f1 = (s - 1) / 2 * tt3 + (1 - s) * t2 + (s - 1) / 2,
        f2 = (s + 3) / 2 * tt3 + (-5 - s) / 2 * t2,
        f3 = (-3 - s) / 2 * tt3 + (s + 2) * t2 + (1 - s) / 2,
        f4 = (1 - s) / 2 * tt3 + (s - 1) / 2 * t2;
      return a * f1 + b * f2 + c * f3 + d * f4;
    };
  }

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

  /**
   * @module Shape
   * @submodule Custom Shapes
   * @for p5
   * @requires core
   * @requires constants
   */

  function vertex(p5, fn){
    /**
     * Begins adding vertices to a custom shape.
     *
     * The `beginShape()` and <a href="#/p5/endShape">endShape()</a> functions
     * allow for creating custom shapes in 2D or 3D. `beginShape()` begins adding
     * vertices to a custom shape and <a href="#/p5/endShape">endShape()</a> stops
     * adding them.
     *
     * The parameter, `kind`, sets the kind of shape to make. The available kinds are:
     *
     * - `PATH` (the default) to draw shapes by tracing out the path along their edges.
     * - `POINTS` to draw a series of points.
     * - `LINES` to draw a series of unconnected line segments.
     * - `TRIANGLES` to draw a series of separate triangles.
     * - `TRIANGLE_FAN` to draw a series of connected triangles sharing the first vertex in a fan-like fashion.
     * - `TRIANGLE_STRIP` to draw a series of connected triangles in strip fashion.
     * - `QUADS` to draw a series of separate quadrilaterals (quads).
     * - `QUAD_STRIP` to draw quad strip using adjacent edges to form the next quad.
     *
     * After calling `beginShape()`, shapes can be built by calling
     * <a href="#/p5/vertex">vertex()</a>,
     * <a href="#/p5/bezierVertex">bezierVertex()</a>, and/or
     * <a href="#/p5/splineVertex">splineVertex()</a>. Calling
     * <a href="#/p5/endShape">endShape()</a> will stop adding vertices to the
     * shape. Each shape will be outlined with the current stroke color and filled
     * with the current fill color.
     *
     * Transformations such as <a href="#/p5/translate">translate()</a>,
     * <a href="#/p5/rotate">rotate()</a>, and
     * <a href="#/p5/scale">scale()</a> don't work between `beginShape()` and
     * <a href="#/p5/endShape">endShape()</a>. It's also not possible to use
     * other shapes, such as <a href="#/p5/ellipse">ellipse()</a> or
     * <a href="#/p5/rect">rect()</a>, between `beginShape()` and
     * <a href="#/p5/endShape">endShape()</a>.
     *
     * @method beginShape
     * @param  {(POINTS|LINES|TRIANGLES|TRIANGLE_FAN|TRIANGLE_STRIP|QUADS|QUAD_STRIP|PATH)} [kind=PATH] either POINTS, LINES, TRIANGLES, TRIANGLE_FAN
     *                                TRIANGLE_STRIP, QUADS, QUAD_STRIP or PATH. Defaults to PATH.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Start drawing the shape.
     *   beginShape();
     *
     *   // Add vertices.
     *   vertex(30, 20);
     *   vertex(85, 20);
     *   vertex(85, 75);
     *   vertex(30, 75);
     *
     *   // Stop drawing the shape.
     *   endShape(CLOSE);
     *
     *   describe('A white square on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Start drawing the shape.
     *   // Only draw the vertices (points).
     *   beginShape(POINTS);
     *
     *   // Add vertices.
     *   vertex(30, 20);
     *   vertex(85, 20);
     *   vertex(85, 75);
     *   vertex(30, 75);
     *
     *   // Stop drawing the shape.
     *   endShape();
     *
     *   describe('Four black dots that form a square are drawn on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Start drawing the shape.
     *   // Only draw lines between alternating pairs of vertices.
     *   beginShape(LINES);
     *
     *   // Add vertices.
     *   vertex(30, 20);
     *   vertex(85, 20);
     *   vertex(85, 75);
     *   vertex(30, 75);
     *
     *   // Stop drawing the shape.
     *   endShape();
     *
     *   describe('Two horizontal black lines on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the shape.
     *   noFill();
     *
     *   // Start drawing the shape.
     *   beginShape();
     *
     *   // Add vertices.
     *   vertex(30, 20);
     *   vertex(85, 20);
     *   vertex(85, 75);
     *   vertex(30, 75);
     *
     *   // Stop drawing the shape.
     *   endShape();
     *
     *   describe('Three black lines form a sideways U shape on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the shape.
     *   noFill();
     *
     *   // Start drawing the shape.
     *   beginShape();
     *
     *   // Add vertices.
     *   vertex(30, 20);
     *   vertex(85, 20);
     *   vertex(85, 75);
     *   vertex(30, 75);
     *
     *   // Stop drawing the shape.
     *   // Connect the first and last vertices.
     *   endShape(CLOSE);
     *
     *   describe('A black outline of a square drawn on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Start drawing the shape.
     *   // Draw a series of triangles.
     *   beginShape(TRIANGLES);
     *
     *   // Left triangle.
     *   vertex(30, 75);
     *   vertex(40, 20);
     *   vertex(50, 75);
     *
     *   // Right triangle.
     *   vertex(60, 20);
     *   vertex(70, 75);
     *   vertex(80, 20);
     *
     *   // Stop drawing the shape.
     *   endShape();
     *
     *   describe('Two white triangles drawn on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Start drawing the shape.
     *   // Draw a series of triangles.
     *   beginShape(TRIANGLE_STRIP);
     *
     *   // Add vertices.
     *   vertex(30, 75);
     *   vertex(40, 20);
     *   vertex(50, 75);
     *   vertex(60, 20);
     *   vertex(70, 75);
     *   vertex(80, 20);
     *   vertex(90, 75);
     *
     *   // Stop drawing the shape.
     *   endShape();
     *
     *   describe('Five white triangles that are interleaved drawn on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Start drawing the shape.
     *   // Draw a series of triangles that share their first vertex.
     *   beginShape(TRIANGLE_FAN);
     *
     *   // Add vertices.
     *   vertex(57.5, 50);
     *   vertex(57.5, 15);
     *   vertex(92, 50);
     *   vertex(57.5, 85);
     *   vertex(22, 50);
     *   vertex(57.5, 15);
     *
     *   // Stop drawing the shape.
     *   endShape();
     *
     *   describe('Four white triangles form a square are drawn on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Start drawing the shape.
     *   // Draw a series of quadrilaterals.
     *   beginShape(QUADS);
     *
     *   // Left rectangle.
     *   vertex(30, 20);
     *   vertex(30, 75);
     *   vertex(50, 75);
     *   vertex(50, 20);
     *
     *   // Right rectangle.
     *   vertex(65, 20);
     *   vertex(65, 75);
     *   vertex(85, 75);
     *   vertex(85, 20);
     *
     *   // Stop drawing the shape.
     *   endShape();
     *
     *   describe('Two white rectangles drawn on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Start drawing the shape.
     *   // Draw a series of quadrilaterals.
     *   beginShape(QUAD_STRIP);
     *
     *   // Add vertices.
     *   vertex(30, 20);
     *   vertex(30, 75);
     *   vertex(50, 20);
     *   vertex(50, 75);
     *   vertex(65, 20);
     *   vertex(65, 75);
     *   vertex(85, 20);
     *   vertex(85, 75);
     *
     *   // Stop drawing the shape.
     *   endShape();
     *
     *   describe('Three white rectangles that share edges are drawn on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   // Start drawing the shape.
     *   // Draw a series of quadrilaterals.
     *   beginShape(PATH);
     *
     *   // Add the vertices.
     *   vertex(-30, -30, 0);
     *   vertex(30, -30, 0);
     *   vertex(30, -10, 0);
     *   vertex(-10, -10, 0);
     *   vertex(-10, 10, 0);
     *   vertex(30, 10, 0);
     *   vertex(30, 30, 0);
     *   vertex(-30, 30, 0);
     *
     *   // Stop drawing the shape.
     *   // Connect the first and last vertices.
     *   endShape(CLOSE);
     *
     *   describe('A blocky C shape drawn in white on a gray background.');
     * }
     *
     * @example
     * // Click and drag with the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A blocky C shape drawn in red, blue, and green on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Start drawing the shape.
     *   // Draw a series of quadrilaterals.
     *   beginShape(PATH);
     *
     *   // Add the vertices.
     *   fill('red');
     *   stroke('red');
     *   vertex(-30, -30, 0);
     *   vertex(30, -30, 0);
     *   vertex(30, -10, 0);
     *   fill('green');
     *   stroke('green');
     *   vertex(-10, -10, 0);
     *   vertex(-10, 10, 0);
     *   vertex(30, 10, 0);
     *   fill('blue');
     *   stroke('blue');
     *   vertex(30, 30, 0);
     *   vertex(-30, 30, 0);
     *
     *   // Stop drawing the shape.
     *   // Connect the first and last vertices.
     *   endShape(CLOSE);
     * }
     */
    fn.beginShape = function(kind) {
      // p5._validateParameters('beginShape', arguments);
      this._renderer.beginShape(...arguments);
    };

    /**
     * Adds a Bézier curve segment to a custom shape.
     *
     * `bezierVertex()` adds a curved segment to custom shapes. The Bézier curves
     * it creates are defined like those made by the
     * <a href="#/p5/bezier">bezier()</a> function. `bezierVertex()` must be
     * called between the
     * <a href="#/p5/beginShape">beginShape()</a> and
     * <a href="#/p5/endShape">endShape()</a> functions.
     * Bézier need a starting point. Building a shape
     * only with Bézier curves needs one initial
     * call to <a href="#/p5/vertex">bezierVertex()</a>, before
     * a number of `bezierVertex()` calls that is a multiple of the parameter
     * set by <a href="#/p5/bezierOrder">bezierOrder(...)</a> (default 3).
     * But shapes can mix different types of vertices, so if there
     * are some previous vertices, then the initial anchor is not needed,
     * only the multiples of 3 (or the Bézier order) calls to
     *  `bezierVertex` for each curve.
     *
     * Each curve of order 3 requires three calls to `bezierVertex`, so
     * 2 curves would need 7 calls to `bezierVertex()`:
     * (1 one initial anchor point, two sets of 3 curves describing the curves)
     * With `bezierOrder(2)`, two curves would need 5 calls: 1 + 2 + 2.
     *
     * Bézier curves can also be drawn in 3D using WebGL mode.
     *
     * Note: `bezierVertex()` won’t work when an argument is passed to
     * <a href="#/p5/beginShape">beginShape()</a>.
     *
     * @method bezierVertex
     * @param  {Number} x x-coordinate of the first control point.
     * @param  {Number} y y-coordinate of the first control point.
     * @param  {Number} [u]
     * @param  {Number} [v]
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the shape.
     *   noFill();
     *
     *   // Start drawing the shape.
     *   beginShape();
     *
     *   // Add the first anchor point.
     *   bezierVertex(30, 20);
     *
     *   // Add the Bézier vertex.
     *   bezierVertex(80, 0);
     *   bezierVertex(80, 75);
     *   bezierVertex(30, 75);
     *
     *   // Stop drawing the shape.
     *   endShape();
     *
     *   describe('A black C curve on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Draw the anchor points in black.
     *   stroke(0);
     *   strokeWeight(5);
     *   point(30, 20);
     *   point(30, 75);
     *
     *   // Draw the control points in red.
     *   stroke(255, 0, 0);
     *   point(80, 0);
     *   point(80, 75);
     *
     *   // Style the shape.
     *   noFill();
     *   stroke(0);
     *   strokeWeight(1);
     *
     *   // Start drawing the shape.
     *   beginShape();
     *
     *   // Add the first anchor point.
     *   bezierVertex(30, 20);
     *
     *   // Add the Bézier vertex.
     *   bezierVertex(80, 0);
     *   bezierVertex(80, 75);
     *   bezierVertex(30, 75);
     *
     *   // Stop drawing the shape.
     *   endShape();
     *
     *   // Draw red lines from the anchor points to the control points.
     *   stroke(255, 0, 0);
     *   line(30, 20, 80, 0);
     *   line(30, 75, 80, 75);
     *
     *   describe(
     *     'A gray square with three curves. A black curve has two straight, red lines that extend from its ends. The endpoints of all the curves are marked with dots.'
     *   );
     * }
     *
     * @example
     * // Click the mouse near the red dot in the top-right corner
     * // and drag to change the curve's shape.
     *
     * let x2 = 80;
     * let y2 = 0;
     * let isChanging = false;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with three curves. A black curve has two straight, red lines that extend from its ends. The endpoints of all the curves are marked with dots.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw the anchor points in black.
     *   stroke(0);
     *   strokeWeight(5);
     *   point(30, 20);
     *   point(30, 75);
     *
     *   // Draw the control points in red.
     *   stroke(255, 0, 0);
     *   point(x2, y2);
     *   point(80, 75);
     *
     *   // Style the shape.
     *   noFill();
     *   stroke(0);
     *   strokeWeight(1);
     *
     *   // Start drawing the shape.
     *   beginShape();
     *
     *   // Add the first anchor point.
     *   bezierVertex(30, 20);
     *
     *   // Add the Bézier vertex.
     *   bezierVertex(x2, y2);
     *   bezierVertex(80, 75);
     *   bezierVertex(30, 75);
     *
     *   // Stop drawing the shape.
     *   endShape();
     *
     *   // Draw red lines from the anchor points to the control points.
     *   stroke(255, 0, 0);
     *   line(30, 20, x2, y2);
     *   line(30, 75, 80, 75);
     * }
     *
     * // Start changing the first control point if the user clicks near it.
     * function mousePressed() {
     *   if (dist(mouseX, mouseY, x2, y2) < 20) {
     *     isChanging = true;
     *   }
     * }
     *
     * // Stop changing the first control point when the user releases the mouse.
     * function mouseReleased() {
     *   isChanging = false;
     * }
     *
     * // Update the first control point while the user drags the mouse.
     * function mouseDragged() {
     *   if (isChanging === true) {
     *     x2 = mouseX;
     *     y2 = mouseY;
     *   }
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Start drawing the shape.
     *   beginShape();
     *
     *   // Add the first anchor point.
     *   bezierVertex(30, 20);
     *
     *   // Add the Bézier vertices.
     *   bezierVertex(80, 0);
     *   bezierVertex(80, 75);
     *   bezierVertex(30, 75);
     *
     *   bezierVertex(50, 80);
     *   bezierVertex(60, 25);
     *   bezierVertex(30, 20);
     *
     *   // Stop drawing the shape.
     *   endShape();
     *
     *   describe('A crescent moon shape drawn in white on a gray background.');
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A crescent moon shape drawn in white on a blue background. When the user drags the mouse, the scene rotates and a second moon is revealed.');
     * }
     *
     * function draw() {
     *   background('midnightblue');
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Style the moons.
     *   noStroke();
     *   fill('lemonchiffon');
     *
     *   // Draw the first moon.
     *   beginShape();
     *   bezierVertex(-20, -30, 0);
     *
     *   bezierVertex(30, -50, 0);
     *   bezierVertex(30, 25, 0);
     *   bezierVertex(-20, 25, 0);
     *
     *   bezierVertex(0, 30, 0);
     *   bezierVertex(10, -25, 0);
     *   bezierVertex(-20, -30, 0);
     *   endShape();
     *
     *   // Draw the second moon.
     *   beginShape();
     *
     *   bezierVertex(-20, -30, -20);
     *
     *   bezierVertex(30, -50, -20);
     *   bezierVertex(30, 25, -20);
     *   bezierVertex(-20, 25, -20);
     *
     *   bezierVertex(0, 30, -20);
     *   bezierVertex(10, -25, -20);
     *   bezierVertex(-20, -30, -20);
     *
     *   endShape();
     * }
     */
    /**
     * @method bezierVertex
     * @param  {Number} x
     * @param  {Number} y
     * @param  {Number} z
     * @param  {Number} [u]
     * @param  {Number} [v]
     */
    fn.bezierVertex = function(...args) {
      this._renderer.bezierVertex(...args);
    };

    /**
     * Concludes the vertices of a custom shape.
     *
     * The <a href="#/p5/beginShape">beginShape()</a> and `endShape()` functions
     * allow for creating custom shapes in 2D or 3D.
     * <a href="#/p5/beginShape">beginShape()</a> begins adding vertices to a
     * custom shape and `endShape()` stops adding them.
     *
     * The first parameter, `mode`, is optional. By default, the first and last
     * vertices of a shape aren't connected. If the constant `CLOSE` is passed, as
     * in `endShape(CLOSE)`, then the first and last vertices will be connected.
     * When CLOSE mode is used for splines (with `splineVeertex()`), the shape is ended smoothly.
     *
     *
     * The second parameter, `count`, is also optional. In WebGL mode, it’s more
     * efficient to draw many copies of the same shape using a technique called
     * <a href="https://webglfundamentals.org/webgl/lessons/webgl-instanced-drawing.html" target="_blank">instancing</a>.
     * The `count` parameter tells WebGL mode how many copies to draw. For
     * example, calling `endShape(CLOSE, 400)` after drawing a custom shape will
     * make it efficient to draw 400 copies. This feature requires
     * <a href="https://p5js.org/tutorials/intro-to-shaders/" target="_blank">writing a custom shader</a>.
     *
     * After calling <a href="#/p5/beginShape">beginShape()</a>, shapes can be
     * built by calling <a href="#/p5/vertex">vertex()</a>,
     * <a href="#/p5/bezierVertex">bezierVertex()</a> and/or
     * <a href="#/p5/splineVertex">splineVertex()</a>. Calling
     * `endShape()` will stop adding vertices to the
     * shape. Each shape will be outlined with the current stroke color and filled
     * with the current fill color.
     *
     * Transformations such as <a href="#/p5/translate">translate()</a>,
     * <a href="#/p5/rotate">rotate()</a>, and
     * <a href="#/p5/scale">scale()</a> don't work between
     * <a href="#/p5/beginShape">beginShape()</a> and `endShape()`. It's also not
     * possible to use other shapes, such as <a href="#/p5/ellipse">ellipse()</a> or
     * <a href="#/p5/rect">rect()</a>, between
     * <a href="#/p5/beginShape">beginShape()</a> and `endShape()`.
     *
     * @method endShape
     * @param  {CLOSE} [mode] use CLOSE to close the shape
     * @param  {Integer} [count] number of times you want to draw/instance the shape (for WebGL mode).
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the shapes.
     *   noFill();
     *
     *   // Left triangle.
     *   beginShape();
     *   vertex(20, 20);
     *   vertex(45, 20);
     *   vertex(45, 80);
     *   endShape(CLOSE);
     *
     *   // Right triangle.
     *   beginShape();
     *   vertex(50, 20);
     *   vertex(75, 20);
     *   vertex(75, 80);
     *   endShape();
     *
     *   describe(
     *     'Two sets of black lines drawn on a gray background. The three lines on the left form a right triangle. The two lines on the right form a right angle.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(200, 100);
     *
     *   background(240);
     *
     *   noFill();
     *   stroke(0);
     *
     *   // Open shape (left)
     *   beginShape();
     *   vertex(20, 20);
     *   vertex(80, 20);
     *   vertex(80, 80);
     *   endShape();  // Not closed
     *
     *   // Closed shape (right)
     *   beginShape();
     *   vertex(120, 20);
     *   vertex(180, 20);
     *   vertex(180, 80);
     *   endShape(CLOSE);  // Closed
     *
     *   describe(
     *     'Two right-angled shapes on a light gray background. The left shape is open with three lines. The right shape is closed, forming a triangle.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *   background(200);
     *
     *   beginShape();
     *
     *   splineVertex(32, 91);
     *   splineVertex(21, 17);
     *   splineVertex(68, 19);
     *   splineVertex(82, 91);
     *
     *   endShape(CLOSE);
     *
     *   describe(
     *     'A curvy four-sided slightly lopsided blob.'
     *   );
     * }
     *
     * @example
     * // Note: A "uniform" is a global variable within a shader program.
     *
     * // Create a string with the vertex shader program.
     * // The vertex shader is called for each vertex.
     * let vertSrc = `#version 300 es
     *
     * precision mediump float;
     *
     * in vec3 aPosition;
     * flat out int instanceID;
     *
     * uniform mat4 uModelViewMatrix;
     * uniform mat4 uProjectionMatrix;
     *
     * void main() {
     *
     *   // Copy the instance ID to the fragment shader.
     *   instanceID = gl_InstanceID;
     *   vec4 positionVec4 = vec4(aPosition, 1.0);
     *
     *   // gl_InstanceID represents a numeric value for each instance.
     *   // Using gl_InstanceID allows us to move each instance separately.
     *   // Here we move each instance horizontally by ID * 23.
     *   float xOffset = float(gl_InstanceID) * 23.0;
     *
     *   // Apply the offset to the final position.
     *   gl_Position = uProjectionMatrix * uModelViewMatrix * (positionVec4 -
     *     vec4(xOffset, 0.0, 0.0, 0.0));
     * }
     * `;
     *
     * // Create a string with the fragment shader program.
     * // The fragment shader is called for each pixel.
     * let fragSrc = `#version 300 es
     *
     * precision mediump float;
     *
     * out vec4 outColor;
     * flat in int instanceID;
     * uniform float numInstances;
     *
     * void main() {
     *   vec4 red = vec4(1.0, 0.0, 0.0, 1.0);
     *   vec4 blue = vec4(0.0, 0.0, 1.0, 1.0);
     *
     *   // Normalize the instance ID.
     *   float normId = float(instanceID) / numInstances;
     *
     *   // Mix between two colors using the normalized instance ID.
     *   outColor = mix(red, blue, normId);
     * }
     * `;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Shader object.
     *   let myShader = createShader(vertSrc, fragSrc);
     *
     *   background(220);
     *
     *   // Compile and apply the p5.Shader.
     *   shader(myShader);
     *
     *   // Set the numInstances uniform.
     *   myShader.setUniform('numInstances', 4);
     *
     *   // Translate the origin to help align the drawing.
     *   translate(25, -10);
     *
     *   // Style the shapes.
     *   noStroke();
     *
     *   // Draw the shapes.
     *   beginShape();
     *   vertex(0, 0);
     *   vertex(0, 20);
     *   vertex(20, 20);
     *   vertex(20, 0);
     *   vertex(0, 0);
     *   endShape(CLOSE, 4);
     *
     *   describe('A row of four squares. Their colors transition from purple on the left to red on the right');
     * }
     */
    fn.endShape = function(mode, count = 1) {
      // p5._validateParameters('endShape', arguments);
      if (count < 1) {
        console.log('🌸 p5.js says: You can not have less than one instance');
        count = 1;
      }

      this._renderer.endShape(mode, count);
    };

    /**
     * Sets the normal vector for vertices in a custom 3D shape.
     *
     * 3D shapes created with <a href="#/p5/beginShape">beginShape()</a> and
     * <a href="#/p5/endShape">endShape()</a> are made by connecting sets of
     * points called vertices. Each vertex added with
     * <a href="#/p5/vertex">vertex()</a> has a normal vector that points away
     * from it. The normal vector controls how light reflects off the shape.
     *
     * `normal()` can be called two ways with different parameters to define the
     * normal vector's components.
     *
     * The first way to call `normal()` has three parameters, `x`, `y`, and `z`.
     * If `Number`s are passed, as in `normal(1, 2, 3)`, they set the x-, y-, and
     * z-components of the normal vector.
     *
     * The second way to call `normal()` has one parameter, `vector`. If a
     * <a href="#/p5.Vector">p5.Vector</a> object is passed, as in
     * `normal(myVector)`, its components will be used to set the normal vector.
     *
     * `normal()` changes the normal vector of vertices added to a custom shape
     * with <a href="#/p5/vertex">vertex()</a>. `normal()` must be called between
     * the <a href="#/p5/beginShape">beginShape()</a> and
     * <a href="#/p5/endShape">endShape()</a> functions, just like
     * <a href="#/p5/vertex">vertex()</a>. The normal vector set by calling
     * `normal()` will affect all following vertices until `normal()` is called
     * again:
     *
     * ```js
     * beginShape();
     *
     * // Set the vertex normal.
     * normal(-0.4, -0.4, 0.8);
     *
     * // Add a vertex.
     * vertex(-30, -30, 0);
     *
     * // Set the vertex normal.
     * normal(0, 0, 1);
     *
     * // Add vertices.
     * vertex(30, -30, 0);
     * vertex(30, 30, 0);
     *
     * // Set the vertex normal.
     * normal(0.4, -0.4, 0.8);
     *
     * // Add a vertex.
     * vertex(-30, 30, 0);
     *
     * endShape();
     * ```
     *
     * @method normal
     * @param  {p5.Vector} vector vertex normal as a <a href="#/p5.Vector">p5.Vector</a> object.
     * @chainable
     *
     * @example
     * // Click the and drag the mouse to view the scene from a different angle.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     'A colorful square on a black background. The square changes color and rotates when the user drags the mouse. Parts of its surface reflect light in different directions.'
     *   );
     * }
     *
     * function draw() {
     *   background(0);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Style the shape.
     *   normalMaterial();
     *   noStroke();
     *
     *   // Draw the shape.
     *   beginShape();
     *   vertex(-30, -30, 0);
     *   vertex(30, -30, 0);
     *   vertex(30, 30, 0);
     *   vertex(-30, 30, 0);
     *   endShape();
     * }
     *
     * @example
     * // Click the and drag the mouse to view the scene from a different angle.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     'A colorful square on a black background. The square changes color and rotates when the user drags the mouse. Parts of its surface reflect light in different directions.'
     *   );
     * }
     *
     * function draw() {
     *   background(0);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Style the shape.
     *   normalMaterial();
     *   noStroke();
     *
     *   // Draw the shape.
     *   // Use normal() to set vertex normals.
     *   beginShape();
     *   normal(-0.4, -0.4, 0.8);
     *   vertex(-30, -30, 0);
     *
     *   normal(0, 0, 1);
     *   vertex(30, -30, 0);
     *   vertex(30, 30, 0);
     *
     *   normal(0.4, -0.4, 0.8);
     *   vertex(-30, 30, 0);
     *   endShape();
     * }
     *
     * @example
     * // Click the and drag the mouse to view the scene from a different angle.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     'A colorful square on a black background. The square changes color and rotates when the user drags the mouse. Parts of its surface reflect light in different directions.'
     *   );
     * }
     *
     * function draw() {
     *   background(0);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Style the shape.
     *   normalMaterial();
     *   noStroke();
     *
     *   // Create p5.Vector objects.
     *   let n1 = createVector(-0.4, -0.4, 0.8);
     *   let n2 = createVector(0, 0, 1);
     *   let n3 = createVector(0.4, -0.4, 0.8);
     *
     *   // Draw the shape.
     *   // Use normal() to set vertex normals.
     *   beginShape();
     *   normal(n1);
     *   vertex(-30, -30, 0);
     *
     *   normal(n2);
     *   vertex(30, -30, 0);
     *   vertex(30, 30, 0);
     *
     *   normal(n3);
     *   vertex(-30, 30, 0);
     *   endShape();
     * }
     */
    /**
     * @method normal
     * @param  {Number} x x-component of the vertex normal.
     * @param  {Number} y y-component of the vertex normal.
     * @param  {Number} z z-component of the vertex normal.
     * @chainable
     */
    fn.normal = function(x, y, z) {
      this._assert3d('normal');
      // p5._validateParameters('normal', arguments);
      this._renderer.normal(...arguments);

      return this;
    };

    /**
     * Sets the shader's vertex property or attribute variables.
     *
     * A vertex property, or vertex attribute, is a variable belonging to a vertex in a shader. p5.js provides some
     * default properties, such as `aPosition`, `aNormal`, `aVertexColor`, etc. These are
     * set using <a href="#/p5/vertex">vertex()</a>, <a href="#/p5/normal">normal()</a>
     * and <a href="#/p5/fill">fill()</a> respectively. Custom properties can also
     * be defined within <a href="#/p5/beginShape">beginShape()</a> and
     * <a href="#/p5/endShape">endShape()</a>.
     *
     * The first parameter, `propertyName`, is a string with the property's name.
     * This is the same variable name which should be declared in the shader, such as
     * `in vec3 aProperty`, similar to .`setUniform()`.
     *
     * The second parameter, `data`, is the value assigned to the shader variable. This
     * value will be applied to subsequent vertices created with
     * <a href="#/p5/vertex">vertex()</a>. It can be a Number or an array of numbers,
     * and in the shader program the type can be declared according to the WebGL
     * specification. Common types include `float`, `vec2`, `vec3`, `vec4` or matrices.
     *
     * See also the <a href="#/p5/vertexProperty">vertexProperty()</a> method on
     * <a href="#/p5/Geometry">Geometry</a> objects.
     *
     * @method vertexProperty
     * @for p5
     * @param {String} attributeName the name of the vertex attribute.
     * @param {Number|Number[]} data the data tied to the vertex attribute.
     *
     * @example
     * const vertSrc = `#version 300 es
     *  precision mediump float;
     *  uniform mat4 uModelViewMatrix;
     *  uniform mat4 uProjectionMatrix;
     *
     *  in vec3 aPosition;
     *  in vec2 aOffset;
     *
     *  void main(){
     *    vec4 positionVec4 = vec4(aPosition.xyz, 1.0);
     *    positionVec4.xy += aOffset;
     *    gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
     *  }
     * `;
     *
     * const fragSrc = `#version 300 es
     *  precision mediump float;
     *  out vec4 outColor;
     *  void main(){
     *    outColor = vec4(0.0, 1.0, 1.0, 1.0);
     *  }
     * `;
     *
     * function setup(){
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create and use the custom shader.
     *   const myShader = createShader(vertSrc, fragSrc);
     *   shader(myShader);
     *
     *   describe('A wobbly, cyan circle on a gray background.');
     * }
     *
     * function draw(){
     *   // Set the styles
     *   background(125);
     *   noStroke();
     *
     *   // Draw the circle.
     *   beginShape();
     *   for (let i = 0; i < 30; i++){
     *     const x = 40 * cos(i/30 * TWO_PI);
     *     const y = 40 * sin(i/30 * TWO_PI);
     *
     *     // Apply some noise to the coordinates.
     *     const xOff = 10 * noise(x + millis()/1000) - 5;
     *     const yOff = 10 * noise(y + millis()/1000) - 5;
     *
     *     // Apply these noise values to the following vertex.
     *     vertexProperty('aOffset', [xOff, yOff]);
     *     vertex(x, y);
     *   }
     *   endShape(CLOSE);
     * }
     *
     * @example
     * let myShader;
     * const cols = 10;
     * const rows = 10;
     * const cellSize = 6;
     *
     * const vertSrc = `#version 300 es
     *   precision mediump float;
     *   uniform mat4 uProjectionMatrix;
     *   uniform mat4 uModelViewMatrix;
     *
     *   in vec3 aPosition;
     *   in vec3 aNormal;
     *   in vec3 aVertexColor;
     *   in float aDistance;
     *
     *   out vec3 vVertexColor;
     *
     *   void main(){
     *     vec4 positionVec4 = vec4(aPosition, 1.0);
     *     positionVec4.xyz += aDistance * aNormal * 2.0;;
     *     vVertexColor = aVertexColor;
     *     gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
     *   }
     * `;
     *
     * const fragSrc = `#version 300 es
     *   precision mediump float;
     *
     *   in vec3 vVertexColor;
     *   out vec4 outColor;
     *
     *   void main(){
     *     outColor = vec4(vVertexColor, 1.0);
     *   }
     * `;
     *
     * function setup(){
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create and apply the custom shader.
     *   myShader = createShader(vertSrc, fragSrc);
     *   shader(myShader);
     *   noStroke();
     *   describe('A blue grid, which moves away from the mouse position, on a gray background.');
     * }
     *
     * function draw(){
     *   background(200);
     *
     *   // Draw the grid in the middle of the screen.
     *   translate(-cols*cellSize/2, -rows*cellSize/2);
     *   beginShape(QUADS);
     *   for (let i = 0; i < cols; i++) {
     *     for (let j = 0; j < rows; j++) {
     *
     *       // Calculate the cell position.
     *       let x = i * cellSize;
     *       let y = j * cellSize;
     *
     *       fill(j/rows*255, j/cols*255, 255);
     *
     *       // Calculate the distance from the corner of each cell to the mouse.
     *       let distance = dist(x, y, mouseX, mouseY);
     *
     *       // Send the distance to the shader.
     *       vertexProperty('aDistance', min(distance, 100));
     *
     *       vertex(x, y);
     *       vertex(x + cellSize, y);
     *       vertex(x + cellSize, y + cellSize);
     *       vertex(x, y + cellSize);
     *     }
     *   }
     *   endShape();
     * }
     */
    fn.vertexProperty = function(attributeName, data){
      // this._assert3d('vertexProperty');
      // p5._validateParameters('vertexProperty', arguments);
      this._renderer.vertexProperty(attributeName, data);
    };
  }

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

  /**
   * @module Color
   * @submodule Setting
   * @for p5
   * @requires core
   * @requires constants
   */


  function setting(p5, fn){
    /**
     * Starts defining a shape that will mask any shapes drawn afterward.
     *
     * Any shapes drawn between `beginClip()` and
     * <a href="#/p5/endClip">endClip()</a> will add to the mask shape. The mask
     * will apply to anything drawn after <a href="#/p5/endClip">endClip()</a>.
     *
     * The parameter, `options`, is optional. If an object with an `invert`
     * property is passed, as in `beginClip({ invert: true })`, it will be used to
     * set the masking mode. `{ invert: true }` inverts the mask, creating holes
     * in shapes that are masked. `invert` is `false` by default.
     *
     * Masks can be contained between the
     * <a href="#/p5/push">push()</a> and <a href="#/p5/pop">pop()</a> functions.
     * Doing so allows unmasked shapes to be drawn after masked shapes.
     *
     * Masks can also be defined in a callback function that's passed to
     * <a href="#/p5/clip">clip()</a>.
     *
     * @method beginClip
     * @param {Object} [options] an object containing clip settings.
     * @param {Boolean} [options.invert=false] Whether or not to invert the mask.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a mask.
     *   beginClip();
     *   triangle(15, 37, 30, 13, 43, 37);
     *   circle(45, 45, 7);
     *   endClip();
     *
     *   // Draw a backing shape.
     *   square(5, 5, 45);
     *
     *   describe('A white triangle and circle on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an inverted mask.
     *   beginClip({ invert: true });
     *   triangle(15, 37, 30, 13, 43, 37);
     *   circle(45, 45, 7);
     *   endClip();
     *
     *   // Draw a backing shape.
     *   square(5, 5, 45);
     *
     *   describe('A white square at the top-left corner of a gray square. The white square has a triangle and a circle cut out of it.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   noStroke();
     *
     *   // Draw a masked shape.
     *   push();
     *   // Create a mask.
     *   beginClip();
     *   triangle(15, 37, 30, 13, 43, 37);
     *   circle(45, 45, 7);
     *   endClip();
     *
     *   // Draw a backing shape.
     *   square(5, 5, 45);
     *   pop();
     *
     *   // Translate the origin to the center.
     *   translate(50, 50);
     *
     *   // Draw an inverted masked shape.
     *   push();
     *   // Create an inverted mask.
     *   beginClip({ invert: true });
     *   triangle(15, 37, 30, 13, 43, 37);
     *   circle(45, 45, 7);
     *   endClip();
     *
     *   // Draw a backing shape.
     *   square(5, 5, 45);
     *   pop();
     *
     *   describe('In the top left, a white triangle and circle. In the bottom right, a white square with a triangle and circle cut out of it.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A silhouette of a rotating torus colored fuchsia.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Create a mask.
     *   beginClip();
     *   push();
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.01);
     *   scale(0.5);
     *   torus(30, 15);
     *   pop();
     *   endClip();
     *
     *   // Draw a backing shape.
     *   noStroke();
     *   fill('fuchsia');
     *   plane(100);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A silhouette of a rotating torus colored with a gradient from cyan to purple.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Create a mask.
     *   beginClip();
     *   push();
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.01);
     *   scale(0.5);
     *   torus(30, 15);
     *   pop();
     *   endClip();
     *
     *   // Draw a backing shape.
     *   noStroke();
     *   beginShape(QUAD_STRIP);
     *   fill(0, 255, 255);
     *   vertex(-width / 2, -height / 2);
     *   vertex(width / 2, -height / 2);
     *   fill(100, 0, 100);
     *   vertex(-width / 2, height / 2);
     *   vertex(width / 2, height / 2);
     *   endShape();
     * }
     */
    fn.beginClip = function(options = {}) {
      this._renderer.beginClip(options);
    };

    /**
     * Ends defining a mask that was started with
     * <a href="#/p5/beginClip">beginClip()</a>.
     *
     * @method endClip
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a mask.
     *   beginClip();
     *   triangle(15, 37, 30, 13, 43, 37);
     *   circle(45, 45, 7);
     *   endClip();
     *
     *   // Draw a backing shape.
     *   square(5, 5, 45);
     *
     *   describe('A white triangle and circle on a gray background.');
     * }
     */
    fn.endClip = function() {
      this._renderer.endClip();
    };

    /**
     * Defines a shape that will mask any shapes drawn afterward.
     *
     * The first parameter, `callback`, is a function that defines the mask.
     * Any shapes drawn in  `callback` will add to the mask shape. The mask
     * will apply to anything drawn after `clip()` is called.
     *
     * The second parameter, `options`, is optional. If an object with an `invert`
     * property is passed, as in `beginClip({ invert: true })`, it will be used to
     * set the masking mode. `{ invert: true }` inverts the mask, creating holes
     * in shapes that are masked. `invert` is `false` by default.
     *
     * Masks can be contained between the
     * <a href="#/p5/push">push()</a> and <a href="#/p5/pop">pop()</a> functions.
     * Doing so allows unmasked shapes to be drawn after masked shapes.
     *
     * Masks can also be defined with <a href="#/p5/beginClip">beginClip()</a>
     * and <a href="#/p5/endClip">endClip()</a>.
     *
     * @method clip
     * @param {Function} callback a function that draws the mask shape.
     * @param {Object} [options] an object containing clip settings.
     * @param {Boolean} [options.invert=false] Whether or not to invert the mask.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a mask.
     *   clip(mask);
     *
     *   // Draw a backing shape.
     *   square(5, 5, 45);
     *
     *   describe('A white triangle and circle on a gray background.');
     * }
     *
     * // Declare a function that defines the mask.
     * function mask() {
     *   triangle(15, 37, 30, 13, 43, 37);
     *   circle(45, 45, 7);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an inverted mask.
     *   clip(mask, { invert: true });
     *
     *   // Draw a backing shape.
     *   square(5, 5, 45);
     *
     *   describe('A white square at the top-left corner of a gray square. The white square has a triangle and a circle cut out of it.');
     * }
     *
     * // Declare a function that defines the mask.
     * function mask() {
     *   triangle(15, 37, 30, 13, 43, 37);
     *   circle(45, 45, 7);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   noStroke();
     *
     *   // Draw a masked shape.
     *   push();
     *   // Create a mask.
     *   clip(mask);
     *
     *   // Draw a backing shape.
     *   square(5, 5, 45);
     *   pop();
     *
     *   // Translate the origin to the center.
     *   translate(50, 50);
     *
     *   // Draw an inverted masked shape.
     *   push();
     *   // Create an inverted mask.
     *   clip(mask, { invert: true });
     *
     *   // Draw a backing shape.
     *   square(5, 5, 45);
     *   pop();
     *
     *   describe('In the top left, a white triangle and circle. In the bottom right, a white square with a triangle and circle cut out of it.');
     * }
     *
     * // Declare a function that defines the mask.
     * function mask() {
     *   triangle(15, 37, 30, 13, 43, 37);
     *   circle(45, 45, 7);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A silhouette of a rotating torus colored fuchsia.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Create a mask.
     *   clip(mask);
     *
     *   // Draw a backing shape.
     *   noStroke();
     *   fill('fuchsia');
     *   plane(100);
     * }
     *
     * // Declare a function that defines the mask.
     * function mask() {
     *   push();
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.01);
     *   scale(0.5);
     *   torus(30, 15);
     *   pop();
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A silhouette of a rotating torus colored with a gradient from cyan to purple.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Create a mask.
     *   clip(mask);
     *
     *   // Draw a backing shape.
     *   noStroke();
     *   beginShape(QUAD_STRIP);
     *   fill(0, 255, 255);
     *   vertex(-width / 2, -height / 2);
     *   vertex(width / 2, -height / 2);
     *   fill(100, 0, 100);
     *   vertex(-width / 2, height / 2);
     *   vertex(width / 2, height / 2);
     *   endShape();
     * }
     *
     * // Declare a function that defines the mask.
     * function mask() {
     *   push();
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.01);
     *   scale(0.5);
     *   torus(30, 15);
     *   pop();
     * }
     */
    fn.clip = function(callback, options) {
      this._renderer.beginClip(options);
      callback();
      this._renderer.endClip(options);
    };

    /**
     * Sets the color used for the background of the canvas.
     *
     * By default, the background is transparent. `background()` is typically used
     * within <a href="#/p5/draw">draw()</a> to clear the display window at the
     * beginning of each frame. It can also be used inside
     * <a href="#/p5/setup">setup()</a> to set the background on the first frame
     * of animation.
     *
     * The version of `background()` with one parameter interprets the value one
     * of four ways. If the parameter is a `Number`, it's interpreted as a grayscale
     * value. If the parameter is a `String`, it's interpreted as a CSS color string.
     * RGB, RGBA, HSL, HSLA, hex, and named color strings are supported. If the
     * parameter is a <a href="#/p5.Color">p5.Color</a> object, it will be used as
     * the background color. If the parameter is a
     * <a href="#/p5.Image">p5.Image</a> object, it will be used as the background
     * image.
     *
     * The version of `background()` with two parameters interprets the first one
     * as a grayscale value. The second parameter sets the alpha (transparency)
     * value.
     *
     * The version of `background()` with three parameters interprets them as RGB,
     * HSB, or HSL colors, depending on the current
     * <a href="#/p5/colorMode">colorMode()</a>. By default, colors are specified
     * in RGB values. Calling `background(255, 204, 0)` sets the background a bright
     * yellow color.
     *
     * The version of `background()` with four parameters interprets them as RGBA,
     * HSBA, or HSLA colors, depending on the current
     * <a href="#/p5/colorMode">colorMode()</a>. The last parameter sets the alpha
     * (transparency) value.
     *
     * @method background
     * @param {p5.Color} color  any value created by the <a href="#/p5/color">color()</a> function
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // A grayscale value.
     *   background(51);
     *
     *   describe('A canvas with a dark charcoal gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // A grayscale value and an alpha value.
     *   background(51, 0.4);
     *   describe('A canvas with a transparent gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // R, G & B values.
     *   background(255, 204, 0);
     *
     *   describe('A canvas with a yellow background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     * // R, G, B, and Alpha values.
     *   background(255, 0, 0, 128);
     *
     *   describe('A canvas with a semi-transparent red background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Use HSB color.
     *   colorMode(HSB);
     *
     *   // H, S & B values.
     *   background(255, 204, 100);
     *
     *   describe('A canvas with a royal blue background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // A CSS named color.
     *   background('red');
     *
     *   describe('A canvas with a red background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Three-digit hex RGB notation.
     *   background('#fae');
     *
     *   describe('A canvas with a pink background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Six-digit hex RGB notation.
     *   background('#222222');
     *
     *   describe('A canvas with a black background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Integer RGB notation.
     *   background('rgb(0, 255, 0)');
     *
     *   describe('A canvas with a bright green background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Integer RGBA notation.
     *   background('rgba(0, 255, 0, 0.25)');
     *
     *   describe('A canvas with a transparent green background.');
     * }
     * <
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Percentage RGB notation.
     *   background('rgb(100%, 0%, 10%)');
     *
     *   describe('A canvas with a red background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Percentage RGBA notation.
     *   background('rgba(100%, 0%, 100%, 0.5)');
     *
     *   describe('A canvas with a transparent purple background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // A p5.Color object.
     *   let c = color(0, 0, 255);
     *   background(c);
     *
     *   describe('A canvas with a blue background.');
     * }
     */
    /**
     * @method background
     * @param {String} colorstring color string, possible formats include: integer
     *                         rgb() or rgba(), percentage rgb() or rgba(),
     *                         3-digit hex, 6-digit hex.
     * @param {Number} [a]         opacity of the background relative to current
     *                             color range (default is 0-255).
     * @chainable
     */
    /**
     * @method background
     * @param {Number} gray   specifies a value between white and black.
     * @param {Number} [a]
     * @chainable
     */
    /**
     * @method background
     * @param {Number} v1     red value if color mode is RGB, or hue value if color mode is HSB.
     * @param {Number} v2     green value if color mode is RGB, or saturation value if color mode is HSB.
     * @param {Number} v3     blue value if color mode is RGB, or brightness value if color mode is HSB.
     * @param  {Number} [a]
     * @chainable
     */
    /**
     * @method background
     * @param  {Number[]}      values  an array containing the red, green, blue
     *                                 and alpha components of the color.
     * @chainable
     */
    /**
     * @method background
     * @param {p5.Image} image     image created with <a href="#/p5/loadImage">loadImage()</a>
     *                             or <a href="#/p5/createImage">createImage()</a>,
     *                             to set as background.
     *                             (must be same size as the sketch window).
     * @param  {Number}  [a]
     * @chainable
     */
    fn.background = function(...args) {
      this._renderer.background(...args);
      return this;
    };

    /**
     * Clears the pixels on the canvas.
     *
     * `clear()` makes every pixel 100% transparent. Calling `clear()` doesn't
     * clear objects created by `createX()` functions such as
     * <a href="#/p5/createGraphics">createGraphics()</a>,
     * <a href="#/p5/createVideo">createVideo()</a>, and
     * <a href="#/p5/createImg">createImg()</a>. These objects will remain
     * unchanged after calling `clear()` and can be redrawn.
     *
     * In WebGL mode, this function can clear the screen to a specific color. It
     * interprets four numeric parameters as normalized RGBA color values. It also
     * clears the depth buffer. If you are not using the WebGL renderer, these
     * parameters will have no effect.
     *
     * @method clear
     * @chainable
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   describe('A gray square. White circles are drawn as the user moves the mouse. The circles disappear when the user presses the mouse.');
     * }
     *
     * function draw() {
     *   circle(mouseX, mouseY, 20);
     * }
     *
     * function mousePressed() {
     *   clear();
     *   background(200);
     * }
     *
     * @example
     * let pg;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *   background(200);
     *
     *   pg = createGraphics(60, 60);
     *   pg.background(200);
     *   pg.noStroke();
     *   pg.circle(pg.width / 2, pg.height / 2, 15);
     *   image(pg, 20, 20);
     *
     *   describe('A white circle drawn on a gray square. The square gets smaller when the mouse is pressed.');
     * }
     *
     * function mousePressed() {
     *   clear();
     *   image(pg, 20, 20);
     * }
     *
     * @param {Number} [r] normalized red value.
     * @param {Number} [g] normalized green value.
     * @param {Number} [b] normalized blue value.
     * @param {Number} [a] normalized alpha value.
     */
    fn.clear = function(...args) {
      const _r = args[0] || 0;
      const _g = args[1] || 0;
      const _b = args[2] || 0;
      const _a = args[3] || 0;

      this._renderer.clear(_r, _g, _b, _a);
      return this;
    };

    /**
     * Changes the way color values are interpreted.
     *
     * By default, the `Number` parameters for <a href="#/p5/fill">fill()</a>,
     * <a href="#/p5/stroke">stroke()</a>,
     * <a href="#/p5/background">background()</a>, and
     * <a href="#/p5/color">color()</a> are defined by values between 0 and 255
     * using the RGB color model. This is equivalent to calling
     * `colorMode(RGB, 255)`. Pure red is `color(255, 0, 0)` in this model.
     *
     * Calling `colorMode(RGB, 100)` sets colors to use RGB color values
     * between 0 and 100. Pure red is `color(100, 0, 0)` in this model.
     *
     * Calling `colorMode(HSB)` or `colorMode(HSL)` changes to HSB or HSL systems instead of RGB.
     * Pure red is `color(0, 100, 100)` in HSB and `color(0, 100, 50)` in HSL.
     *
     * Some additional color modes that p5.js supports are:
     *
     * `RGBHDR` - High Dynamic Range RGB defined within the Display P3 color space.
     *          Colors are expressed with an extended dynamic range. To render these colors
     *          accurately, you must use the HDR canvas.
     *
     * `HWB`    - Hue, Whiteness, Blackness.
     *          Similar to HSB and HSL, this mode uses a hue angle.
     *          Instead of saturation and lightness, HWB defines colors based on the percentage
     *          of whiteness and blackness. This is the color model used by Chrome's GUI color picker.
     *          Pure red in HWB is represented as `color(0, 0, 0)` (i.e., hue 0 with 0% whiteness and 0% blackness).
     *
     *          <img src="assets/hwb.png"></img>
     *
     * `LAB`    - Also known as CIE Lab, this color mode defines colors with Lightness, Alpha, and Beta.
     *          It is widely used in professional color measurement contexts due to its perceptual uniformity.
     *
     * `LCH`    - A more intuitive representation of the CIE Lab color space using Lightness, Chroma, and Hue.
     *          This mode separates the color's chromatic intensity (chroma) from its lightness,
     *          simplifying color selection and manipulation.
     *
     * `OKLAB`  - A variant of the CIE Lab color space that corrects for non-uniformities inherent in LAB.
     *          The adjustment provides a more perceptually accurate and uniform representation,
     *          which is particularly beneficial for smooth color transitions.
     *
     * `OKLCH`  - An easier-to-use representation of OKLAB, expressing colors in terms of Lightness, Chroma, and Hue.
     *          This mode retains the perceptual benefits of OKLAB while offering a more intuitive format for color manipulation.
     *
     * <a href="#/p5.Color">p5.Color</a> objects remember the mode that they were
     * created in. Changing modes doesn't affect their appearance.
     *
     *  `Single-value (Grayscale) Colors`:
     *  When a color is specified with only one parameter (e.g., `color(g)`), p5.js will interpret it
     *  as a grayscale color. However, how that single parameter translates into a grayscale value
     *  depends on the color mode:
     *
     * - `RGB, HSB, and HSL`: In RGB, the single value is interpreted using the “blue” maximum
     *   (i.e., the single parameter is mapped to the blue channel's max).
     *   In HSB and HSL, the single value is mapped to Brightness and Lightness max respectively with hue=0 .
     *   and saturation=0.
     *
     * - `LAB, LCH, OKLAB, and OKLCH`: The single value is taken to be the `lightness (L)` component,
     *   with the specified max range for that channel.
     *
     * - `HWB`: Grayscale relies on both the `whiteness (W)` and `blackness (B)` channels. Since
     *   a single value cannot directly account for two distinct channels, the library uses an
     *   average of their max values to interpret the single grayscale parameter. For instance,
     *   if W has a max of 50 and B has a max of 100, then the single grayscale parameter
     *   is mapped using (50 + 100) / 2 = 75 as its effective maximum. More complex or negative
     *   ranges are currently not handled, so results in those cases may be ambiguous.
     *
     * @method colorMode
     * @param {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} mode   either RGB, HSB, HSL,
     *          or one of the extended modes described above.
     * @param {Number}  [max]  range for all values.
     * @return {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} The current color mode.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Fill with pure red.
     *   fill(255, 0, 0);
     *
     *   circle(50, 50, 25);
     *
     *   describe('A gray square with a red circle at its center.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use RGB color with values in the range 0-100.
     *   colorMode(RGB, 100);
     *
     *   // Fill with pure red.
     *   fill(100, 0, 0);
     *
     *   circle(50, 50, 25);
     *
     *   describe('A gray square with a red circle at its center.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use HSB color.
     *   colorMode(HSB);
     *
     *   // Fill with pure red.
     *   fill(0, 100, 100);
     *
     *   circle(50, 50, 25);
     *
     *   describe('A gray square with a red circle at its center.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use HSL color.
     *   colorMode(HSL);
     *
     *   // Fill with pure red.
     *   fill(0, 100, 50);
     *
     *   circle(50, 50, 25);
     *
     *   describe('A gray square with a red circle at its center.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Draw a neutral gray background using the default color mode.
     *   background(200);
     *
     *   // Switch to HWB color mode.
     *   // (Assuming p5.js supports HWB with a range of:
     *   // hue: 0–360, whiteness: 0–100, blackness: 0–100.)
     *   colorMode(HWB);
     *
     *   // Set fill to pure red in HWB.
     *   // Pure red in HWB is: hue = 0°, whiteness = 0%, blackness = 0%.
     *   fill(0, 0, 0);
     *
     *   // Draw a circle at the center.
     *   circle(50, 50, 25);
     *
     *   describe('A gray square with a red circle at its center, drawn using HWB color mode.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Draw a neutral gray background using the default color mode.
     *   background(200);
     *
     *   // Switch to LAB color mode.
     *   // In this mode, L typically ranges from 0 to 100 while a and b span roughly -128 to 127.
     *   colorMode(LAB);
     *
     *   // Set fill to pure red in LAB.
     *   // The sRGB red (255, 0, 0) converts approximately to LAB as:
     *   // L = 53, a = 80, b = 67.
     *   fill(53, 80, 67);
     *
     *   // Draw a circle at the center.
     *   circle(50, 50, 25);
     *
     *   describe('A gray square with a red circle at its center, drawn using LAB color mode.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Draw a neutral gray background.
     *   background(200);
     *
     *   // Switch to LCH color mode.
     *   // In LCH, colors are defined by Lightness, Chroma, and Hue (in degrees).
     *   colorMode(LCH);
     *
     *   // Set fill to an approximation of pure red in LCH:
     *   // Lightness ≈ 53, Chroma ≈ 104, Hue ≈ 40°.
     *   fill(53, 104, 40);
     *
     *   // Draw a circle at the center.
     *   circle(50, 50, 25);
     *
     *   describe('A gray square with a red circle at its center, drawn using LCH color mode.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Use RGB color with values in the range 0-100.
     *   colorMode(RGB, 100);
     *
     *   for (let x = 0; x < 100; x += 1) {
     *     for (let y = 0; y < 100; y += 1) {
     *       stroke(x, y, 0);
     *       point(x, y);
     *     }
     *   }
     *
     *   describe(
     *     'A diagonal green to red gradient from bottom-left to top-right with shading transitioning to black at top-left corner.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Use HSB color with values in the range 0-100.
     *   colorMode(HSB, 100);
     *
     *   for (let x = 0; x < 100; x += 1) {
     *     for (let y = 0; y < 100; y += 1) {
     *       stroke(x, y, 100);
     *       point(x, y);
     *     }
     *   }
     *
     *   describe('A rainbow gradient from left-to-right. Brightness transitions to white at the top.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.Color object.
     *   let myColor = color(180, 175, 230);
     *   background(myColor);
     *
     *   // Use RGB color with values in the range 0-1.
     *   colorMode(RGB, 1);
     *
     *   // Get the red, green, and blue color components.
     *   let redValue = red(myColor);
     *   let greenValue = green(myColor);
     *   let blueValue = blue(myColor);
     *
     *   // Round the color components for display.
     *   redValue = round(redValue, 2);
     *   greenValue = round(greenValue, 2);
     *   blueValue = round(blueValue, 2);
     *
     *   // Display the color components.
     *   text(`Red: ${redValue}`, 10, 10, 80, 80);
     *   text(`Green: ${greenValue}`, 10, 40, 80, 80);
     *   text(`Blue: ${blueValue}`, 10, 70, 80, 80);
     *
     *   describe('A purple canvas with the red, green, and blue decimal values of the color written on it.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(255);
     *
     *   // Use RGB color with alpha values in the range 0-1.
     *   colorMode(RGB, 255, 255, 255, 1);
     *
     *   noFill();
     *   strokeWeight(4);
     *   stroke(255, 0, 10, 0.3);
     *   circle(40, 40, 50);
     *   circle(50, 60, 50);
     *
     *   describe('Two overlapping translucent pink circle outlines.');
     * }
     *
     * @example
     * let hslGraphic, lchGraphic, oklchGraphic;
     *
     * function setup() {
     *   createCanvas(600, 200);
     *   noLoop();
     *
     *   // Create three graphics objects for HSL, LCH, and OKLCH color modes
     *   hslGraphic = createGraphics(200, 200);
     *   lchGraphic = createGraphics(200, 200);
     *   oklchGraphic = createGraphics(200, 200);
     *
     *   // Draw HSL color wheel
     *   colorMode(HSL);
     *   hslGraphic.translate(100, 100);
     *   for (let i = 0; i < 1000; i++) {
     *     hslGraphic.stroke(360 / 1000 * i, 70, 50);
     *     hslGraphic.line(0, 0, hslGraphic.width / 2, 0);
     *     hslGraphic.rotate(TAU / 1000);
     *   }
     *
     *   // Draw LCH color wheel
     *   colorMode(LCH);
     *   lchGraphic.translate(100, 100);
     *   for (let i = 0; i < 1000; i++) {
     *     lchGraphic.stroke(54, 106, 360 / 1000 * i);
     *     lchGraphic.line(0, 0, lchGraphic.width / 2, 0);
     *     lchGraphic.rotate(TAU / 1000);
     *   }
     *
     *   // Draw OKLCH color wheel
     *   colorMode(OKLCH);
     *   oklchGraphic.translate(100, 100);
     *   for (let i = 0; i < 1000; i++) {
     *     oklchGraphic.stroke(54, 106, 360 / 1000 * i);
     *     oklchGraphic.line(0, 0, oklchGraphic.width / 2, 0);
     *     oklchGraphic.rotate(TAU / 1000);
     *   }
     * }
     *
     * function draw() {
     *   // Set the styles
     *   colorMode(RGB);
     *   background(220);
     *
     *   // Display the color wheels
     *   image(hslGraphic, 0, 0);
     *   image(lchGraphic, 200, 0);
     *   image(oklchGraphic, 400, 0);
     * }
     *
     * @example
     * // Example: Single-value (Grayscale) colors in different color modes.
     * // The rectangle is filled with one parameter, but its final color depends
     * // on how that parameter is interpreted by the current color mode.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *   noStroke();
     *   noLoop();
     * }
     *
     * function draw() {
     *   // Set color mode to RGB with range 0-255
     *   colorMode(RGB, 255);
     *
     *   // Fill with single grayscale value
     *   fill(128);
     *   rect(0, 0, 100, 100);
     *
     *   // Add text label
     *   fill(0); // Switch to black text for clarity
     *   textSize(14);
     *   text("RGB (128)", 10, 20);
     * }
     */
    /**
     * @method colorMode
     * @param {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} mode
     * @param {Number} max1     range for the red or hue depending on the
     *                              current color mode.
     * @param {Number} max2     range for the green or saturation depending
     *                              on the current color mode.
     * @param {Number} max3     range for the blue or brightness/lightness
     *                              depending on the current color mode.
     * @param {Number} [maxA]   range for the alpha.
     *
     * @return {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} The current color mode.
     */
    /**
     * @method colorMode
     * @return {RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH} The current color mode.
     */
    fn.colorMode = function(mode, max1, max2, max3, maxA) {
      // p5._validateParameters('colorMode', arguments);
      if (
        [
          RGB,
          RGBHDR,
          HSB,
          HSL,
          HWB,
          LAB,
          LCH,
          OKLAB,
          OKLCH
        ].includes(mode)
      ) {
        // Set color mode.
        this._renderer.states.setValue('colorMode', mode);

        // Set color maxes.
        this._renderer.states.setValue('colorMaxes', this._renderer.states.colorMaxes.clone());
        const maxes = this._renderer.states.colorMaxes[mode];
        if (arguments.length === 2) {
          maxes[0] = max1; // Red
          maxes[1] = max1; // Green
          maxes[2] = max1; // Blue
          maxes[3] = max1; // Alpha
        } else if (arguments.length === 4) {
          maxes[0] = max1; // Red
          maxes[1] = max2; // Green
          maxes[2] = max3; // Blue
        } else if (arguments.length === 5) {
          maxes[0] = max1; // Red
          maxes[1] = max2; // Green
          maxes[2] = max3; // Blue
          maxes[3] = maxA; // Alpha
        }
      }

      return this._renderer.states.colorMode;
    };

    /**
     * Sets the color used to fill shapes.
     *
     * Calling `fill(255, 165, 0)` or `fill('orange')` means all shapes drawn
     * after the fill command will be filled with the color orange.
     *
     * The version of `fill()` with one parameter interprets the value one of
     * three ways. If the parameter is a `Number`, it's interpreted as a grayscale
     * value. If the parameter is a `String`, it's interpreted as a CSS color
     * string. A <a href="#/p5.Color">p5.Color</a> object can also be provided to
     * set the fill color.
     *
     * The version of `fill()` with three parameters interprets them as RGB, HSB,
     * or HSL colors, depending on the current
     * <a href="#/p5/colorMode">colorMode()</a>. The default color space is RGB,
     * with each value in the range from 0 to 255.
     *
     * The version of `fill()` with four parameters interprets them as `RGBA`, `HSBA`,
     * or `HSLA` colors, depending on the current <a href="#/p5/colorMode">colorMode()</a>. The last parameter
     * sets the alpha (transparency) value.
     *
     * @method fill
     * @param  {Number}        v1      red value if color mode is RGB or hue value if color mode is HSB.
     * @param  {Number}        v2      green value if color mode is RGB or saturation value if color mode is HSB.
     * @param  {Number}        v3      blue value if color mode is RGB or brightness value if color mode is HSB.
     * @param  {Number}        [alpha] alpha value, controls transparency (0 - transparent, 255 - opaque).
     * @chainable
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // A grayscale value.
     *   fill(51);
     *   square(20, 20, 60);
     *
     *   describe('A dark charcoal gray square with a black outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // R, G & B values.
     *   fill(255, 204, 0);
     *   square(20, 20, 60);
     *
     *   describe('A yellow square with a black outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // R, G, B, and Alpha values.
     *   fill(255, 0, 0, 128);
     *   square(20, 20, 60);
     *
     *   describe('A semi-transparent red square with a black outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(100);
     *
     *   // Use HSB color.
     *   colorMode(HSB);
     *
     *   // H, S & B values.
     *   fill(255, 204, 100);
     *   square(20, 20, 60);
     *
     *   describe('A royal blue square with a black outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // A CSS named color.
     *   fill('red');
     *   square(20, 20, 60);
     *
     *   describe('A red square with a black outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Three-digit hex RGB notation.
     *   fill('#fae');
     *   square(20, 20, 60);
     *
     *   describe('A pink square with a black outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Six-digit hex RGB notation.
     *   fill('#A251FA');
     *   square(20, 20, 60);
     *
     *   describe('A purple square with a black outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Integer RGB notation.
     *   fill('rgb(0, 255, 0)');
     *   square(20, 20, 60);
     *
     *   describe('A bright green square with a black outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Integer RGBA notation.
     *   fill('rgba(0, 255, 0, 0.25)');
     *   square(20, 20, 60);
     *
     *   describe('A soft green rectange with a black outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Percentage RGB notation.
     *   fill('rgb(100%, 0%, 10%)');
     *   square(20, 20, 60);
     *
     *   describe('A red square with a black outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Percentage RGBA notation.
     *   fill('rgba(100%, 0%, 100%, 0.5)');
     *   square(20, 20, 60);
     *
     *   describe('A dark fuchsia square with a black outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // A p5.Color object.
     *   let c = color(0, 0, 255);
     *   fill(c);
     *   square(20, 20, 60);
     *
     *   describe('A blue square with a black outline.');
     * }
     */
    /**
     * @method fill
     * @param  {String}        value   a color string.
     * @chainable
     */
    /**
     * @method fill
     * @param  {Number}        gray   a grayscale value.
     * @param  {Number}        [alpha]
     * @chainable
     */
    /**
     * @method fill
     * @param  {Number[]}      values  an array containing the red, green, blue &
     *                                 and alpha components of the color.
     * @chainable
     */
    /**
     * @method fill
     * @param  {p5.Color}      color   the fill color.
     * @chainable
     */
    fn.fill = function(...args) {
      this._renderer.fill(...args);
      return this;
    };

    /**
     * Disables setting the fill color for shapes.
     *
     * Calling `noFill()` is the same as making the fill completely transparent,
     * as in `fill(0, 0)`. If both <a href="#/p5/noStroke">noStroke()</a> and
     * `noFill()` are called, nothing will be drawn to the screen.
     *
     * @method noFill
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Draw the top square.
     *   square(32, 10, 35);
     *
     *   // Draw the bottom square.
     *   noFill();
     *   square(32, 55, 35);
     *
     *   describe('A white square on above an empty square. Both squares have black outlines.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A purple cube wireframe spinning on a black canvas.');
     * }
     *
     * function draw() {
     *   background(0);
     *
     *   // Style the box.
     *   noFill();
     *   stroke(100, 100, 240);
     *
     *   // Rotate the coordinates.
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.01);
     *
     *   // Draw the box.
     *   box(45);
     * }
     */
    fn.noFill = function() {
      this._renderer.noFill();
      return this;
    };

    /**
     * Disables drawing points, lines, and the outlines of shapes.
     *
     * Calling `noStroke()` is the same as making the stroke completely transparent,
     * as in `stroke(0, 0)`. If both `noStroke()` and
     * <a href="#/p5/noFill">noFill()</a> are called, nothing will be drawn to the
     * screen.
     *
     * @method noStroke
     * @chainable
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   noStroke();
     *   square(20, 20, 60);
     *
     *   describe('A white square with no outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A pink cube with no edge outlines spinning on a black canvas.');
     * }
     *
     * function draw() {
     *   background(0);
     *
     *   // Style the box.
     *   noStroke();
     *   fill(240, 150, 150);
     *
     *   // Rotate the coordinates.
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.01);
     *
     *   // Draw the box.
     *   box(45);
     * }
     */
    fn.noStroke = function() {
      this._renderer.states.setValue('strokeColor', null);
      return this;
    };

    /**
     * Sets the color used to draw points, lines, and the outlines of shapes.
     *
     * Calling `stroke(255, 165, 0)` or `stroke('orange')` means all shapes drawn
     * after calling `stroke()` will be outlined with the color orange. The way
     * these parameters are interpreted may be changed with the
     * <a href="#/p5/colorMode">colorMode()</a> function.
     *
     * The version of `stroke()` with one parameter interprets the value one of
     * three ways. If the parameter is a `Number`, it's interpreted as a grayscale
     * value. If the parameter is a `String`, it's interpreted as a CSS color
     * string. A <a href="#/p5.Color">p5.Color</a> object can also be provided to
     * set the stroke color.
     *
     * The version of `stroke()` with two parameters interprets the first one as a
     * grayscale value. The second parameter sets the alpha (transparency) value.
     *
     * The version of `stroke()` with three parameters interprets them as RGB, HSB,
     * or HSL colors, depending on the current `colorMode()`.
     *
     * The version of `stroke()` with four parameters interprets them as RGBA, HSBA,
     * or HSLA colors, depending on the current `colorMode()`. The last parameter
     * sets the alpha (transparency) value.
     *
     * @method stroke
     * @param  {Number}        v1      red value if color mode is RGB or hue value if color mode is HSB.
     * @param  {Number}        v2      green value if color mode is RGB or saturation value if color mode is HSB.
     * @param  {Number}        v3      blue value if color mode is RGB or brightness value if color mode is HSB.
     * @param  {Number}        [alpha] alpha value, controls transparency (0 - transparent, 255 - opaque).
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // A grayscale value.
     *   strokeWeight(4);
     *   stroke(51);
     *   square(20, 20, 60);
     *
     *   describe('A white square with a dark charcoal gray outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // R, G & B values.
     *   stroke(255, 204, 0);
     *   strokeWeight(4);
     *   square(20, 20, 60);
     *
     *   describe('A white square with a yellow outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use HSB color.
     *   colorMode(HSB);
     *
     *   // H, S & B values.
     *   strokeWeight(4);
     *   stroke(255, 204, 100);
     *   square(20, 20, 60);
     *
     *   describe('A white square with a royal blue outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // A CSS named color.
     *   stroke('red');
     *   strokeWeight(4);
     *   square(20, 20, 60);
     *
     *   describe('A white square with a red outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Three-digit hex RGB notation.
     *   stroke('#fae');
     *   strokeWeight(4);
     *   square(20, 20, 60);
     *
     *   describe('A white square with a pink outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Six-digit hex RGB notation.
     *   stroke('#222222');
     *   strokeWeight(4);
     *   square(20, 20, 60);
     *
     *   describe('A white square with a black outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Integer RGB notation.
     *   stroke('rgb(0, 255, 0)');
     *   strokeWeight(4);
     *   square(20, 20, 60);
     *
     *   describe('A white square with a bright green outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Integer RGBA notation.
     *   stroke('rgba(0, 255, 0, 0.25)');
     *   strokeWeight(4);
     *   square(20, 20, 60);
     *
     *   describe('A white square with a soft green outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Percentage RGB notation.
     *   stroke('rgb(100%, 0%, 10%)');
     *   strokeWeight(4);
     *   square(20, 20, 60);
     *
     *   describe('A white square with a red outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Percentage RGBA notation.
     *   stroke('rgba(100%, 0%, 100%, 0.5)');
     *   strokeWeight(4);
     *   square(20, 20, 60);
     *
     *   describe('A white square with a dark fuchsia outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // A p5.Color object.
     *   stroke(color(0, 0, 255));
     *   strokeWeight(4);
     *   square(20, 20, 60);
     *
     *   describe('A white square with a blue outline.');
     * }
     */
    /**
     * @method stroke
     * @param  {String}        value   a color string.
     * @chainable
     */
    /**
     * @method stroke
     * @param  {Number}        gray   a grayscale value.
     * @param  {Number}        [alpha]
     * @chainable
     */
    /**
     * @method stroke
     * @param  {Number[]}      values  an array containing the red, green, blue,
     *                                 and alpha components of the color.
     * @chainable
     */
    /**
     * @method stroke
     * @param  {p5.Color}      color   the stroke color.
     * @chainable
     */
    fn.stroke = function(...args) {
      this._renderer.stroke(...args);
      return this;
    };

    /**
     * Starts using shapes to erase parts of the canvas.
     *
     * All drawing that follows `erase()` will subtract from the canvas, revealing
     * the web page underneath. The erased areas will become transparent, allowing
     * the content behind the canvas to show through. The
     * <a href="#/p5/fill">fill()</a>, <a href="#/p5/stroke">stroke()</a>, and
     * <a href="#/p5/blendMode">blendMode()</a> have no effect once `erase()` is
     * called.
     *
     * The `erase()` function has two optional parameters. The first parameter
     * sets the strength of erasing by the shape's interior. A value of 0 means
     * that no erasing will occur. A value of 255 means that the shape's interior
     * will fully erase the content underneath. The default value is 255
     * (full strength).
     *
     * The second parameter sets the strength of erasing by the shape's edge. A
     * value of 0 means that no erasing will occur. A value of 255 means that the
     * shape's edge will fully erase the content underneath. The default value is
     * 255 (full strength).
     *
     * To cancel the erasing effect, use the <a href="#/p5/noErase">noErase()</a>
     * function.
     *
     * `erase()` has no effect on drawing done with the
     * <a href="#/p5/image">image()</a> and
     * <a href="#/p5/background">background()</a> functions.
     *
     * @method erase
     * @param  {Number}   [strengthFill]      a number (0-255) for the strength of erasing under a shape's interior.
     *                                        Defaults to 255, which is full strength.
     * @param  {Number}   [strengthStroke]    a number (0-255) for the strength of erasing under a shape's edge.
     *                                        Defaults to 255, which is full strength.
     *
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(100, 100, 250);
     *
     *   // Draw a pink square.
     *   fill(250, 100, 100);
     *   square(20, 20, 60);
     *
     *   // Erase a circular area.
     *   erase();
     *   circle(25, 30, 30);
     *   noErase();
     *
     *   describe('A purple canvas with a pink square in the middle. A circle is erased from the top-left, leaving a hole.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(100, 100, 250);
     *
     *   // Draw a pink square.
     *   fill(250, 100, 100);
     *   square(20, 20, 60);
     *
     *   // Erase a circular area.
     *   strokeWeight(5);
     *   erase(150, 255);
     *   circle(25, 30, 30);
     *   noErase();
     *
     *   describe('A purple canvas with a pink square in the middle. A circle at the top-left partially erases its interior and a fully erases its outline.');
     * }
     */
    fn.erase = function(opacityFill = 255, opacityStroke = 255) {
      this._renderer.erase(opacityFill, opacityStroke);

      return this;
    };

    /**
     * Ends erasing that was started with <a href="#/p5/erase">erase()</a>.
     *
     * The <a href="#/p5/fill">fill()</a>, <a href="#/p5/stroke">stroke()</a>, and
     * <a href="#/p5/blendMode">blendMode()</a> settings will return to what they
     * were prior to calling <a href="#/p5/erase">erase()</a>.
     *
     * @method noErase
     * @chainable
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(235, 145, 15);
     *
     *   // Draw the left rectangle.
     *   noStroke();
     *   fill(30, 45, 220);
     *   rect(30, 10, 10, 80);
     *
     *   // Erase a circle.
     *   erase();
     *   circle(50, 50, 60);
     *   noErase();
     *
     *   // Draw the right rectangle.
     *   rect(70, 10, 10, 80);
     *
     *   describe('An orange canvas with two tall blue rectangles. A circular hole in the center erases the rectangle on the left but not the one on the right.');
     * }
     */
    fn.noErase = function() {
      this._renderer.noErase();
      return this;
    };

    /**
     * Sets the way colors blend when added to the canvas.
     *
     * By default, drawing with a solid color paints over the current pixel values
     * on the canvas. `blendMode()` offers many options for blending colors.
     *
     * Shapes, images, and text can be used as sources for drawing to the canvas.
     * A source pixel changes the color of the canvas pixel where it's drawn. The
     * final color results from blending the source pixel's color with the canvas
     * pixel's color. RGB color values from the source and canvas pixels are
     * compared, added, subtracted, multiplied, and divided to create different
     * effects. Red values with red values, greens with greens, and blues with
     * blues.
     *
     * The parameter, `mode`, sets the blend mode. For example, calling
     * `blendMode(ADD)` sets the blend mode to `ADD`. The following blend modes
     * are available in both 2D and WebGL mode:
     *
     * - `BLEND`: color values from the source overwrite the canvas. This is the default mode.
     * - `ADD`: color values from the source are added to values from the canvas.
     * - `DARKEST`: keeps the darkest color value.
     * - `LIGHTEST`: keeps the lightest color value.
     * - `EXCLUSION`: similar to `DIFFERENCE` but with less contrast.
     * - `MULTIPLY`: color values from the source are multiplied with values from the canvas. The result is always darker.
     * - `SCREEN`: all color values are inverted, then multiplied, then inverted again. The result is always lighter. (Opposite of `MULTIPLY`)
     * - `REPLACE`: the last source drawn completely replaces the rest of the canvas.
     * - `REMOVE`: overlapping pixels are removed by making them completely transparent.
     *
     * The following blend modes are only available in 2D mode:
     *
     * - `DIFFERENCE`: color values from the source are subtracted from the values from the canvas. If the difference is a negative number, it's made positive.
     * - `OVERLAY`: combines `MULTIPLY` and `SCREEN`. Dark values in the canvas get darker and light values get lighter.
     * - `HARD_LIGHT`: combines `MULTIPLY` and `SCREEN`. Dark values in the source get darker and light values get lighter.
     * - `SOFT_LIGHT`: a softer version of `HARD_LIGHT`.
     * - `DODGE`: lightens light tones and increases contrast. Divides the canvas color values by the inverted color values from the source.
     * - `BURN`: darkens dark tones and increases contrast. Divides the source color values by the inverted color values from the canvas, then inverts the result.
     *
     * The following blend modes are only available in WebGL mode:
     *
     * - `SUBTRACT`: RGB values from the source are subtracted from the values from the canvas. If the difference is a negative number, it's made positive. Alpha (transparency) values from the source and canvas are added.
     *
     * @method blendMode
     * @param  {(BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|REMOVE|SUBTRACT)} mode blend mode to set.
     *                either BLEND, DARKEST, LIGHTEST, DIFFERENCE, MULTIPLY,
     *                EXCLUSION, SCREEN, REPLACE, OVERLAY, HARD_LIGHT,
     *                SOFT_LIGHT, DODGE, BURN, ADD, REMOVE or SUBTRACT
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use the default blend mode.
     *   blendMode(BLEND);
     *
     *   // Style the lines.
     *   strokeWeight(30);
     *
     *   // Draw the blue line.
     *   stroke('blue');
     *   line(25, 25, 75, 75);
     *
     *   // Draw the red line.
     *   stroke('red');
     *   line(75, 25, 25, 75);
     *
     *   describe('A blue line and a red line form an X on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the blend mode.
     *   blendMode(ADD);
     *
     *   // Style the lines.
     *   strokeWeight(30);
     *
     *   // Draw the blue line.
     *   stroke('blue');
     *   line(25, 25, 75, 75);
     *
     *   // Draw the red line.
     *   stroke('red');
     *   line(75, 25, 25, 75);
     *
     *   describe('A faint blue line and a faint red line form an X on a gray background. The area where they overlap is faint magenta.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the blend mode.
     *   blendMode(DARKEST);
     *
     *   // Style the lines.
     *   strokeWeight(30);
     *
     *   // Draw the blue line.
     *   stroke('blue');
     *   line(25, 25, 75, 75);
     *
     *   // Draw the red line.
     *   stroke('red');
     *   line(75, 25, 25, 75);
     *
     *   describe('A blue line and a red line form an X on a gray background. The area where they overlap is black.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the blend mode.
     *   blendMode(LIGHTEST);
     *
     *   // Style the lines.
     *   strokeWeight(30);
     *
     *   // Draw the blue line.
     *   stroke('blue');
     *   line(25, 25, 75, 75);
     *
     *   // Draw the red line.
     *   stroke('red');
     *   line(75, 25, 25, 75);
     *
     *   describe('A faint blue line and a faint red line form an X on a gray background. The area where they overlap is faint magenta.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the blend mode.
     *   blendMode(EXCLUSION);
     *
     *   // Style the lines.
     *   strokeWeight(30);
     *
     *   // Draw the blue line.
     *   stroke('blue');
     *   line(25, 25, 75, 75);
     *
     *   // Draw the red line.
     *   stroke('red');
     *   line(75, 25, 25, 75);
     *
     *   describe('A yellow line and a cyan line form an X on a gray background. The area where they overlap is green.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the blend mode.
     *   blendMode(MULTIPLY);
     *
     *   // Style the lines.
     *   strokeWeight(30);
     *
     *   // Draw the blue line.
     *   stroke('blue');
     *   line(25, 25, 75, 75);
     *
     *   // Draw the red line.
     *   stroke('red');
     *   line(75, 25, 25, 75);
     *
     *   describe('A blue line and a red line form an X on a gray background. The area where they overlap is black.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the blend mode.
     *   blendMode(SCREEN);
     *
     *   // Style the lines.
     *   strokeWeight(30);
     *
     *   // Draw the blue line.
     *   stroke('blue');
     *   line(25, 25, 75, 75);
     *
     *   // Draw the red line.
     *   stroke('red');
     *   line(75, 25, 25, 75);
     *
     *   describe('A faint blue line and a faint red line form an X on a gray background. The area where they overlap is faint magenta.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the blend mode.
     *   blendMode(REPLACE);
     *
     *   // Style the lines.
     *   strokeWeight(30);
     *
     *   // Draw the blue line.
     *   stroke('blue');
     *   line(25, 25, 75, 75);
     *
     *   // Draw the red line.
     *   stroke('red');
     *   line(75, 25, 25, 75);
     *
     *   describe('A diagonal red line.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the blend mode.
     *   blendMode(REMOVE);
     *
     *   // Style the lines.
     *   strokeWeight(30);
     *
     *   // Draw the blue line.
     *   stroke('blue');
     *   line(25, 25, 75, 75);
     *
     *   // Draw the red line.
     *   stroke('red');
     *   line(75, 25, 25, 75);
     *
     *   describe('The silhouette of an X is missing from a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the blend mode.
     *   blendMode(DIFFERENCE);
     *
     *   // Style the lines.
     *   strokeWeight(30);
     *
     *   // Draw the blue line.
     *   stroke('blue');
     *   line(25, 25, 75, 75);
     *
     *   // Draw the red line.
     *   stroke('red');
     *   line(75, 25, 25, 75);
     *
     *   describe('A yellow line and a cyan line form an X on a gray background. The area where they overlap is green.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the blend mode.
     *   blendMode(OVERLAY);
     *
     *   // Style the lines.
     *   strokeWeight(30);
     *
     *   // Draw the blue line.
     *   stroke('blue');
     *   line(25, 25, 75, 75);
     *
     *   // Draw the red line.
     *   stroke('red');
     *   line(75, 25, 25, 75);
     *
     *   describe('A faint blue line and a faint red line form an X on a gray background. The area where they overlap is bright magenta.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the blend mode.
     *   blendMode(HARD_LIGHT);
     *
     *   // Style the lines.
     *   strokeWeight(30);
     *
     *   // Draw the blue line.
     *   stroke('blue');
     *   line(25, 25, 75, 75);
     *
     *   // Draw the red line.
     *   stroke('red');
     *   line(75, 25, 25, 75);
     *
     *   describe('A blue line and a red line form an X on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the blend mode.
     *   blendMode(SOFT_LIGHT);
     *
     *   // Style the lines.
     *   strokeWeight(30);
     *
     *   // Draw the blue line.
     *   stroke('blue');
     *   line(25, 25, 75, 75);
     *
     *   // Draw the red line.
     *   stroke('red');
     *   line(75, 25, 25, 75);
     *
     *   describe('A faint blue line and a faint red line form an X on a gray background. The area where they overlap is violet.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the blend mode.
     *   blendMode(DODGE);
     *
     *   // Style the lines.
     *   strokeWeight(30);
     *
     *   // Draw the blue line.
     *   stroke('blue');
     *   line(25, 25, 75, 75);
     *
     *   // Draw the red line.
     *   stroke('red');
     *   line(75, 25, 25, 75);
     *
     *   describe('A faint blue line and a faint red line form an X on a gray background. The area where they overlap is faint violet.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the blend mode.
     *   blendMode(BURN);
     *
     *   // Style the lines.
     *   strokeWeight(30);
     *
     *   // Draw the blue line.
     *   stroke('blue');
     *   line(25, 25, 75, 75);
     *
     *   // Draw the red line.
     *   stroke('red');
     *   line(75, 25, 25, 75);
     *
     *   describe('A blue line and a red line form an X on a gray background. The area where they overlap is black.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the blend mode.
     *   blendMode(SUBTRACT);
     *
     *   // Style the lines.
     *   strokeWeight(30);
     *
     *   // Draw the blue line.
     *   stroke('blue');
     *   line(25, 25, 75, 75);
     *
     *   // Draw the red line.
     *   stroke('red');
     *   line(75, 25, 25, 75);
     *
     *   describe('A yellow line and a turquoise line form an X on a gray background. The area where they overlap is green.');
     * }
     */
    fn.blendMode = function (mode) {
      // p5._validateParameters('blendMode', arguments);
      if (mode === NORMAL) {
        // Warning added 3/26/19, can be deleted in future (1.0 release?)
        console.warn(
          'NORMAL has been deprecated for use in blendMode. defaulting to BLEND instead.'
        );
        mode = BLEND;
      }
      this._renderer.blendMode(mode);
    };
  }

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

  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};

  function getDefaultExportFromCjs (x) {
  	return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
  }

  var GifReader_1;
  var GifWriter_1;

  function GifWriter(buf, width, height, gopts) {
    var p = 0;

    var gopts = gopts === undefined ? { } : gopts;
    var loop_count = gopts.loop === undefined ? null : gopts.loop;
    var global_palette = gopts.palette === undefined ? null : gopts.palette;

    if (width <= 0 || height <= 0 || width > 65535 || height > 65535)
      throw new Error("Width/Height invalid.");

    function check_palette_and_num_colors(palette) {
      var num_colors = palette.length;
      if (num_colors < 2 || num_colors > 256 ||  num_colors & (num_colors-1)) {
        throw new Error(
            "Invalid code/color length, must be power of 2 and 2 .. 256.");
      }
      return num_colors;
    }

    // - Header.
    buf[p++] = 0x47; buf[p++] = 0x49; buf[p++] = 0x46;  // GIF
    buf[p++] = 0x38; buf[p++] = 0x39; buf[p++] = 0x61;  // 89a

    // Handling of Global Color Table (palette) and background index.
    var gp_num_colors_pow2 = 0;
    var background = 0;
    if (global_palette !== null) {
      var gp_num_colors = check_palette_and_num_colors(global_palette);
      while (gp_num_colors >>= 1) ++gp_num_colors_pow2;
      gp_num_colors = 1 << gp_num_colors_pow2;
      --gp_num_colors_pow2;
      if (gopts.background !== undefined) {
        background = gopts.background;
        if (background >= gp_num_colors)
          throw new Error("Background index out of range.");
        // The GIF spec states that a background index of 0 should be ignored, so
        // this is probably a mistake and you really want to set it to another
        // slot in the palette.  But actually in the end most browsers, etc end
        // up ignoring this almost completely (including for dispose background).
        if (background === 0)
          throw new Error("Background index explicitly passed as 0.");
      }
    }

    // - Logical Screen Descriptor.
    // NOTE(deanm): w/h apparently ignored by implementations, but set anyway.
    buf[p++] = width & 0xff; buf[p++] = width >> 8 & 0xff;
    buf[p++] = height & 0xff; buf[p++] = height >> 8 & 0xff;
    // NOTE: Indicates 0-bpp original color resolution (unused?).
    buf[p++] = (global_palette !== null ? 0x80 : 0) |  // Global Color Table Flag.
               gp_num_colors_pow2;  // NOTE: No sort flag (unused?).
    buf[p++] = background;  // Background Color Index.
    buf[p++] = 0;  // Pixel aspect ratio (unused?).

    // - Global Color Table
    if (global_palette !== null) {
      for (var i = 0, il = global_palette.length; i < il; ++i) {
        var rgb = global_palette[i];
        buf[p++] = rgb >> 16 & 0xff;
        buf[p++] = rgb >> 8 & 0xff;
        buf[p++] = rgb & 0xff;
      }
    }

    if (loop_count !== null) {  // Netscape block for looping.
      if (loop_count < 0 || loop_count > 65535)
        throw new Error("Loop count invalid.")
      // Extension code, label, and length.
      buf[p++] = 0x21; buf[p++] = 0xff; buf[p++] = 0x0b;
      // NETSCAPE2.0
      buf[p++] = 0x4e; buf[p++] = 0x45; buf[p++] = 0x54; buf[p++] = 0x53;
      buf[p++] = 0x43; buf[p++] = 0x41; buf[p++] = 0x50; buf[p++] = 0x45;
      buf[p++] = 0x32; buf[p++] = 0x2e; buf[p++] = 0x30;
      // Sub-block
      buf[p++] = 0x03; buf[p++] = 0x01;
      buf[p++] = loop_count & 0xff; buf[p++] = loop_count >> 8 & 0xff;
      buf[p++] = 0x00;  // Terminator.
    }


    var ended = false;

    this.addFrame = function(x, y, w, h, indexed_pixels, opts) {
      if (ended === true) { --p; ended = false; }  // Un-end.

      opts = opts === undefined ? { } : opts;

      // TODO(deanm): Bounds check x, y.  Do they need to be within the virtual
      // canvas width/height, I imagine?
      if (x < 0 || y < 0 || x > 65535 || y > 65535)
        throw new Error("x/y invalid.")

      if (w <= 0 || h <= 0 || w > 65535 || h > 65535)
        throw new Error("Width/Height invalid.")

      if (indexed_pixels.length < w * h)
        throw new Error("Not enough pixels for the frame size.");

      var using_local_palette = true;
      var palette = opts.palette;
      if (palette === undefined || palette === null) {
        using_local_palette = false;
        palette = global_palette;
      }

      if (palette === undefined || palette === null)
        throw new Error("Must supply either a local or global palette.");

      var num_colors = check_palette_and_num_colors(palette);

      // Compute the min_code_size (power of 2), destroying num_colors.
      var min_code_size = 0;
      while (num_colors >>= 1) ++min_code_size;
      num_colors = 1 << min_code_size;  // Now we can easily get it back.

      var delay = opts.delay === undefined ? 0 : opts.delay;

      // From the spec:
      //     0 -   No disposal specified. The decoder is
      //           not required to take any action.
      //     1 -   Do not dispose. The graphic is to be left
      //           in place.
      //     2 -   Restore to background color. The area used by the
      //           graphic must be restored to the background color.
      //     3 -   Restore to previous. The decoder is required to
      //           restore the area overwritten by the graphic with
      //           what was there prior to rendering the graphic.
      //  4-7 -    To be defined.
      // NOTE(deanm): Dispose background doesn't really work, apparently most
      // browsers ignore the background palette index and clear to transparency.
      var disposal = opts.disposal === undefined ? 0 : opts.disposal;
      if (disposal < 0 || disposal > 3)  // 4-7 is reserved.
        throw new Error("Disposal out of range.");

      var use_transparency = false;
      var transparent_index = 0;
      if (opts.transparent !== undefined && opts.transparent !== null) {
        use_transparency = true;
        transparent_index = opts.transparent;
        if (transparent_index < 0 || transparent_index >= num_colors)
          throw new Error("Transparent color index.");
      }

      if (disposal !== 0 || use_transparency || delay !== 0) {
        // - Graphics Control Extension
        buf[p++] = 0x21; buf[p++] = 0xf9;  // Extension / Label.
        buf[p++] = 4;  // Byte size.

        buf[p++] = disposal << 2 | (use_transparency === true ? 1 : 0);
        buf[p++] = delay & 0xff; buf[p++] = delay >> 8 & 0xff;
        buf[p++] = transparent_index;  // Transparent color index.
        buf[p++] = 0;  // Block Terminator.
      }

      // - Image Descriptor
      buf[p++] = 0x2c;  // Image Seperator.
      buf[p++] = x & 0xff; buf[p++] = x >> 8 & 0xff;  // Left.
      buf[p++] = y & 0xff; buf[p++] = y >> 8 & 0xff;  // Top.
      buf[p++] = w & 0xff; buf[p++] = w >> 8 & 0xff;
      buf[p++] = h & 0xff; buf[p++] = h >> 8 & 0xff;
      // NOTE: No sort flag (unused?).
      // TODO(deanm): Support interlace.
      buf[p++] = using_local_palette === true ? (0x80 | (min_code_size-1)) : 0;

      // - Local Color Table
      if (using_local_palette === true) {
        for (var i = 0, il = palette.length; i < il; ++i) {
          var rgb = palette[i];
          buf[p++] = rgb >> 16 & 0xff;
          buf[p++] = rgb >> 8 & 0xff;
          buf[p++] = rgb & 0xff;
        }
      }

      p = GifWriterOutputLZWCodeStream(
              buf, p, min_code_size < 2 ? 2 : min_code_size, indexed_pixels);

      return p;
    };

    this.end = function() {
      if (ended === false) {
        buf[p++] = 0x3b;  // Trailer.
        ended = true;
      }
      return p;
    };

    this.getOutputBuffer = function() { return buf; };
    this.setOutputBuffer = function(v) { buf = v; };
    this.getOutputBufferPosition = function() { return p; };
    this.setOutputBufferPosition = function(v) { p = v; };
  }

  // Main compression routine, palette indexes -> LZW code stream.
  // |index_stream| must have at least one entry.
  function GifWriterOutputLZWCodeStream(buf, p, min_code_size, index_stream) {
    buf[p++] = min_code_size;
    var cur_subblock = p++;  // Pointing at the length field.

    var clear_code = 1 << min_code_size;
    var code_mask = clear_code - 1;
    var eoi_code = clear_code + 1;
    var next_code = eoi_code + 1;

    var cur_code_size = min_code_size + 1;  // Number of bits per code.
    var cur_shift = 0;
    // We have at most 12-bit codes, so we should have to hold a max of 19
    // bits here (and then we would write out).
    var cur = 0;

    function emit_bytes_to_buffer(bit_block_size) {
      while (cur_shift >= bit_block_size) {
        buf[p++] = cur & 0xff;
        cur >>= 8; cur_shift -= 8;
        if (p === cur_subblock + 256) {  // Finished a subblock.
          buf[cur_subblock] = 255;
          cur_subblock = p++;
        }
      }
    }

    function emit_code(c) {
      cur |= c << cur_shift;
      cur_shift += cur_code_size;
      emit_bytes_to_buffer(8);
    }

    // I am not an expert on the topic, and I don't want to write a thesis.
    // However, it is good to outline here the basic algorithm and the few data
    // structures and optimizations here that make this implementation fast.
    // The basic idea behind LZW is to build a table of previously seen runs
    // addressed by a short id (herein called output code).  All data is
    // referenced by a code, which represents one or more values from the
    // original input stream.  All input bytes can be referenced as the same
    // value as an output code.  So if you didn't want any compression, you
    // could more or less just output the original bytes as codes (there are
    // some details to this, but it is the idea).  In order to achieve
    // compression, values greater then the input range (codes can be up to
    // 12-bit while input only 8-bit) represent a sequence of previously seen
    // inputs.  The decompressor is able to build the same mapping while
    // decoding, so there is always a shared common knowledge between the
    // encoding and decoder, which is also important for "timing" aspects like
    // how to handle variable bit width code encoding.
    //
    // One obvious but very important consequence of the table system is there
    // is always a unique id (at most 12-bits) to map the runs.  'A' might be
    // 4, then 'AA' might be 10, 'AAA' 11, 'AAAA' 12, etc.  This relationship
    // can be used for an effecient lookup strategy for the code mapping.  We
    // need to know if a run has been seen before, and be able to map that run
    // to the output code.  Since we start with known unique ids (input bytes),
    // and then from those build more unique ids (table entries), we can
    // continue this chain (almost like a linked list) to always have small
    // integer values that represent the current byte chains in the encoder.
    // This means instead of tracking the input bytes (AAAABCD) to know our
    // current state, we can track the table entry for AAAABC (it is guaranteed
    // to exist by the nature of the algorithm) and the next character D.
    // Therefor the tuple of (table_entry, byte) is guaranteed to also be
    // unique.  This allows us to create a simple lookup key for mapping input
    // sequences to codes (table indices) without having to store or search
    // any of the code sequences.  So if 'AAAA' has a table entry of 12, the
    // tuple of ('AAAA', K) for any input byte K will be unique, and can be our
    // key.  This leads to a integer value at most 20-bits, which can always
    // fit in an SMI value and be used as a fast sparse array / object key.

    // Output code for the current contents of the index buffer.
    var ib_code = index_stream[0] & code_mask;  // Load first input index.
    var code_table = { };  // Key'd on our 20-bit "tuple".

    emit_code(clear_code);  // Spec says first code should be a clear code.

    // First index already loaded, process the rest of the stream.
    for (var i = 1, il = index_stream.length; i < il; ++i) {
      var k = index_stream[i] & code_mask;
      var cur_key = ib_code << 8 | k;  // (prev, k) unique tuple.
      var cur_code = code_table[cur_key];  // buffer + k.

      // Check if we have to create a new code table entry.
      if (cur_code === undefined) {  // We don't have buffer + k.
        // Emit index buffer (without k).
        // This is an inline version of emit_code, because this is the core
        // writing routine of the compressor (and V8 cannot inline emit_code
        // because it is a closure here in a different context).  Additionally
        // we can call emit_byte_to_buffer less often, because we can have
        // 30-bits (from our 31-bit signed SMI), and we know our codes will only
        // be 12-bits, so can safely have 18-bits there without overflow.
        // emit_code(ib_code);
        cur |= ib_code << cur_shift;
        cur_shift += cur_code_size;
        while (cur_shift >= 8) {
          buf[p++] = cur & 0xff;
          cur >>= 8; cur_shift -= 8;
          if (p === cur_subblock + 256) {  // Finished a subblock.
            buf[cur_subblock] = 255;
            cur_subblock = p++;
          }
        }

        if (next_code === 4096) {  // Table full, need a clear.
          emit_code(clear_code);
          next_code = eoi_code + 1;
          cur_code_size = min_code_size + 1;
          code_table = { };
        } else {  // Table not full, insert a new entry.
          // Increase our variable bit code sizes if necessary.  This is a bit
          // tricky as it is based on "timing" between the encoding and
          // decoder.  From the encoders perspective this should happen after
          // we've already emitted the index buffer and are about to create the
          // first table entry that would overflow our current code bit size.
          if (next_code >= (1 << cur_code_size)) ++cur_code_size;
          code_table[cur_key] = next_code++;  // Insert into code table.
        }

        ib_code = k;  // Index buffer to single input k.
      } else {
        ib_code = cur_code;  // Index buffer to sequence in code table.
      }
    }

    emit_code(ib_code);  // There will still be something in the index buffer.
    emit_code(eoi_code);  // End Of Information.

    // Flush / finalize the sub-blocks stream to the buffer.
    emit_bytes_to_buffer(1);

    // Finish the sub-blocks, writing out any unfinished lengths and
    // terminating with a sub-block of length 0.  If we have already started
    // but not yet used a sub-block it can just become the terminator.
    if (cur_subblock + 1 === p) {  // Started but unused.
      buf[cur_subblock] = 0;
    } else {  // Started and used, write length and additional terminator block.
      buf[cur_subblock] = p - cur_subblock - 1;
      buf[p++] = 0;
    }
    return p;
  }

  function GifReader(buf) {
    var p = 0;

    // - Header (GIF87a or GIF89a).
    if (buf[p++] !== 0x47 ||            buf[p++] !== 0x49 || buf[p++] !== 0x46 ||
        buf[p++] !== 0x38 || (buf[p++]+1 & 0xfd) !== 0x38 || buf[p++] !== 0x61) {
      throw new Error("Invalid GIF 87a/89a header.");
    }

    // - Logical Screen Descriptor.
    var width = buf[p++] | buf[p++] << 8;
    var height = buf[p++] | buf[p++] << 8;
    var pf0 = buf[p++];  // <Packed Fields>.
    var global_palette_flag = pf0 >> 7;
    var num_global_colors_pow2 = pf0 & 0x7;
    var num_global_colors = 1 << (num_global_colors_pow2 + 1);
    buf[p++];
    buf[p++];  // Pixel aspect ratio (unused?).

    var global_palette_offset = null;
    var global_palette_size   = null;

    if (global_palette_flag) {
      global_palette_offset = p;
      global_palette_size = num_global_colors;
      p += num_global_colors * 3;  // Seek past palette.
    }

    var no_eof = true;

    var frames = [ ];

    var delay = 0;
    var transparent_index = null;
    var disposal = 0;  // 0 - No disposal specified.
    var loop_count = null;

    this.width = width;
    this.height = height;

    while (no_eof && p < buf.length) {
      switch (buf[p++]) {
        case 0x21:  // Graphics Control Extension Block
          switch (buf[p++]) {
            case 0xff:  // Application specific block
              // Try if it's a Netscape block (with animation loop counter).
              if (buf[p   ] !== 0x0b ||  // 21 FF already read, check block size.
                  // NETSCAPE2.0
                  buf[p+1 ] == 0x4e && buf[p+2 ] == 0x45 && buf[p+3 ] == 0x54 &&
                  buf[p+4 ] == 0x53 && buf[p+5 ] == 0x43 && buf[p+6 ] == 0x41 &&
                  buf[p+7 ] == 0x50 && buf[p+8 ] == 0x45 && buf[p+9 ] == 0x32 &&
                  buf[p+10] == 0x2e && buf[p+11] == 0x30 &&
                  // Sub-block
                  buf[p+12] == 0x03 && buf[p+13] == 0x01 && buf[p+16] == 0) {
                p += 14;
                loop_count = buf[p++] | buf[p++] << 8;
                p++;  // Skip terminator.
              } else {  // We don't know what it is, just try to get past it.
                p += 12;
                while (true) {  // Seek through subblocks.
                  var block_size = buf[p++];
                  // Bad block size (ex: undefined from an out of bounds read).
                  if (!(block_size >= 0)) throw Error("Invalid block size");
                  if (block_size === 0) break;  // 0 size is terminator
                  p += block_size;
                }
              }
              break;

            case 0xf9:  // Graphics Control Extension
              if (buf[p++] !== 0x4 || buf[p+4] !== 0)
                throw new Error("Invalid graphics extension block.");
              var pf1 = buf[p++];
              delay = buf[p++] | buf[p++] << 8;
              transparent_index = buf[p++];
              if ((pf1 & 1) === 0) transparent_index = null;
              disposal = pf1 >> 2 & 0x7;
              p++;  // Skip terminator.
              break;

            case 0xfe:  // Comment Extension.
              while (true) {  // Seek through subblocks.
                var block_size = buf[p++];
                // Bad block size (ex: undefined from an out of bounds read).
                if (!(block_size >= 0)) throw Error("Invalid block size");
                if (block_size === 0) break;  // 0 size is terminator
                // console.log(buf.slice(p, p+block_size).toString('ascii'));
                p += block_size;
              }
              break;

            default:
              throw new Error(
                  "Unknown graphic control label: 0x" + buf[p-1].toString(16));
          }
          break;

        case 0x2c:  // Image Descriptor.
          var x = buf[p++] | buf[p++] << 8;
          var y = buf[p++] | buf[p++] << 8;
          var w = buf[p++] | buf[p++] << 8;
          var h = buf[p++] | buf[p++] << 8;
          var pf2 = buf[p++];
          var local_palette_flag = pf2 >> 7;
          var interlace_flag = pf2 >> 6 & 1;
          var num_local_colors_pow2 = pf2 & 0x7;
          var num_local_colors = 1 << (num_local_colors_pow2 + 1);
          var palette_offset = global_palette_offset;
          var palette_size = global_palette_size;
          var has_local_palette = false;
          if (local_palette_flag) {
            var has_local_palette = true;
            palette_offset = p;  // Override with local palette.
            palette_size = num_local_colors;
            p += num_local_colors * 3;  // Seek past palette.
          }

          var data_offset = p;

          p++;  // codesize
          while (true) {
            var block_size = buf[p++];
            // Bad block size (ex: undefined from an out of bounds read).
            if (!(block_size >= 0)) throw Error("Invalid block size");
            if (block_size === 0) break;  // 0 size is terminator
            p += block_size;
          }

          frames.push({x: x, y: y, width: w, height: h,
                       has_local_palette: has_local_palette,
                       palette_offset: palette_offset,
                       palette_size: palette_size,
                       data_offset: data_offset,
                       data_length: p - data_offset,
                       transparent_index: transparent_index,
                       interlaced: !!interlace_flag,
                       delay: delay,
                       disposal: disposal});
          break;

        case 0x3b:  // Trailer Marker (end of file).
          no_eof = false;
          break;

        default:
          throw new Error("Unknown gif block: 0x" + buf[p-1].toString(16));
      }
    }

    this.numFrames = function() {
      return frames.length;
    };

    this.loopCount = function() {
      return loop_count;
    };

    this.frameInfo = function(frame_num) {
      if (frame_num < 0 || frame_num >= frames.length)
        throw new Error("Frame index out of range.");
      return frames[frame_num];
    };

    this.decodeAndBlitFrameBGRA = function(frame_num, pixels) {
      var frame = this.frameInfo(frame_num);
      var num_pixels = frame.width * frame.height;
      var index_stream = new Uint8Array(num_pixels);  // At most 8-bit indices.
      GifReaderLZWOutputIndexStream(
          buf, frame.data_offset, index_stream, num_pixels);
      var palette_offset = frame.palette_offset;

      // NOTE(deanm): It seems to be much faster to compare index to 256 than
      // to === null.  Not sure why, but CompareStub_EQ_STRICT shows up high in
      // the profile, not sure if it's related to using a Uint8Array.
      var trans = frame.transparent_index;
      if (trans === null) trans = 256;

      // We are possibly just blitting to a portion of the entire frame.
      // That is a subrect within the framerect, so the additional pixels
      // must be skipped over after we finished a scanline.
      var framewidth  = frame.width;
      var framestride = width - framewidth;
      var xleft       = framewidth;  // Number of subrect pixels left in scanline.

      // Output indicies of the top left and bottom right corners of the subrect.
      var opbeg = ((frame.y * width) + frame.x) * 4;
      var opend = ((frame.y + frame.height) * width + frame.x) * 4;
      var op    = opbeg;

      var scanstride = framestride * 4;

      // Use scanstride to skip past the rows when interlacing.  This is skipping
      // 7 rows for the first two passes, then 3 then 1.
      if (frame.interlaced === true) {
        scanstride += width * 4 * 7;  // Pass 1.
      }

      var interlaceskip = 8;  // Tracking the row interval in the current pass.

      for (var i = 0, il = index_stream.length; i < il; ++i) {
        var index = index_stream[i];

        if (xleft === 0) {  // Beginning of new scan line
          op += scanstride;
          xleft = framewidth;
          if (op >= opend) { // Catch the wrap to switch passes when interlacing.
            scanstride = framestride * 4 + width * 4 * (interlaceskip-1);
            // interlaceskip / 2 * 4 is interlaceskip << 1.
            op = opbeg + (framewidth + framestride) * (interlaceskip << 1);
            interlaceskip >>= 1;
          }
        }

        if (index === trans) {
          op += 4;
        } else {
          var r = buf[palette_offset + index * 3];
          var g = buf[palette_offset + index * 3 + 1];
          var b = buf[palette_offset + index * 3 + 2];
          pixels[op++] = b;
          pixels[op++] = g;
          pixels[op++] = r;
          pixels[op++] = 255;
        }
        --xleft;
      }
    };

    // I will go to copy and paste hell one day...
    this.decodeAndBlitFrameRGBA = function(frame_num, pixels) {
      var frame = this.frameInfo(frame_num);
      var num_pixels = frame.width * frame.height;
      var index_stream = new Uint8Array(num_pixels);  // At most 8-bit indices.
      GifReaderLZWOutputIndexStream(
          buf, frame.data_offset, index_stream, num_pixels);
      var palette_offset = frame.palette_offset;

      // NOTE(deanm): It seems to be much faster to compare index to 256 than
      // to === null.  Not sure why, but CompareStub_EQ_STRICT shows up high in
      // the profile, not sure if it's related to using a Uint8Array.
      var trans = frame.transparent_index;
      if (trans === null) trans = 256;

      // We are possibly just blitting to a portion of the entire frame.
      // That is a subrect within the framerect, so the additional pixels
      // must be skipped over after we finished a scanline.
      var framewidth  = frame.width;
      var framestride = width - framewidth;
      var xleft       = framewidth;  // Number of subrect pixels left in scanline.

      // Output indicies of the top left and bottom right corners of the subrect.
      var opbeg = ((frame.y * width) + frame.x) * 4;
      var opend = ((frame.y + frame.height) * width + frame.x) * 4;
      var op    = opbeg;

      var scanstride = framestride * 4;

      // Use scanstride to skip past the rows when interlacing.  This is skipping
      // 7 rows for the first two passes, then 3 then 1.
      if (frame.interlaced === true) {
        scanstride += width * 4 * 7;  // Pass 1.
      }

      var interlaceskip = 8;  // Tracking the row interval in the current pass.

      for (var i = 0, il = index_stream.length; i < il; ++i) {
        var index = index_stream[i];

        if (xleft === 0) {  // Beginning of new scan line
          op += scanstride;
          xleft = framewidth;
          if (op >= opend) { // Catch the wrap to switch passes when interlacing.
            scanstride = framestride * 4 + width * 4 * (interlaceskip-1);
            // interlaceskip / 2 * 4 is interlaceskip << 1.
            op = opbeg + (framewidth + framestride) * (interlaceskip << 1);
            interlaceskip >>= 1;
          }
        }

        if (index === trans) {
          op += 4;
        } else {
          var r = buf[palette_offset + index * 3];
          var g = buf[palette_offset + index * 3 + 1];
          var b = buf[palette_offset + index * 3 + 2];
          pixels[op++] = r;
          pixels[op++] = g;
          pixels[op++] = b;
          pixels[op++] = 255;
        }
        --xleft;
      }
    };
  }

  function GifReaderLZWOutputIndexStream(code_stream, p, output, output_length) {
    var min_code_size = code_stream[p++];

    var clear_code = 1 << min_code_size;
    var eoi_code = clear_code + 1;
    var next_code = eoi_code + 1;

    var cur_code_size = min_code_size + 1;  // Number of bits per code.
    // NOTE: This shares the same name as the encoder, but has a different
    // meaning here.  Here this masks each code coming from the code stream.
    var code_mask = (1 << cur_code_size) - 1;
    var cur_shift = 0;
    var cur = 0;

    var op = 0;  // Output pointer.

    var subblock_size = code_stream[p++];

    // TODO(deanm): Would using a TypedArray be any faster?  At least it would
    // solve the fast mode / backing store uncertainty.
    // var code_table = Array(4096);
    var code_table = new Int32Array(4096);  // Can be signed, we only use 20 bits.

    var prev_code = null;  // Track code-1.

    while (true) {
      // Read up to two bytes, making sure we always 12-bits for max sized code.
      while (cur_shift < 16) {
        if (subblock_size === 0) break;  // No more data to be read.

        cur |= code_stream[p++] << cur_shift;
        cur_shift += 8;

        if (subblock_size === 1) {  // Never let it get to 0 to hold logic above.
          subblock_size = code_stream[p++];  // Next subblock.
        } else {
          --subblock_size;
        }
      }

      // TODO(deanm): We should never really get here, we should have received
      // and EOI.
      if (cur_shift < cur_code_size)
        break;

      var code = cur & code_mask;
      cur >>= cur_code_size;
      cur_shift -= cur_code_size;

      // TODO(deanm): Maybe should check that the first code was a clear code,
      // at least this is what you're supposed to do.  But actually our encoder
      // now doesn't emit a clear code first anyway.
      if (code === clear_code) {
        // We don't actually have to clear the table.  This could be a good idea
        // for greater error checking, but we don't really do any anyway.  We
        // will just track it with next_code and overwrite old entries.

        next_code = eoi_code + 1;
        cur_code_size = min_code_size + 1;
        code_mask = (1 << cur_code_size) - 1;

        // Don't update prev_code ?
        prev_code = null;
        continue;
      } else if (code === eoi_code) {
        break;
      }

      // We have a similar situation as the decoder, where we want to store
      // variable length entries (code table entries), but we want to do in a
      // faster manner than an array of arrays.  The code below stores sort of a
      // linked list within the code table, and then "chases" through it to
      // construct the dictionary entries.  When a new entry is created, just the
      // last byte is stored, and the rest (prefix) of the entry is only
      // referenced by its table entry.  Then the code chases through the
      // prefixes until it reaches a single byte code.  We have to chase twice,
      // first to compute the length, and then to actually copy the data to the
      // output (backwards, since we know the length).  The alternative would be
      // storing something in an intermediate stack, but that doesn't make any
      // more sense.  I implemented an approach where it also stored the length
      // in the code table, although it's a bit tricky because you run out of
      // bits (12 + 12 + 8), but I didn't measure much improvements (the table
      // entries are generally not the long).  Even when I created benchmarks for
      // very long table entries the complexity did not seem worth it.
      // The code table stores the prefix entry in 12 bits and then the suffix
      // byte in 8 bits, so each entry is 20 bits.

      var chase_code = code < next_code ? code : prev_code;

      // Chase what we will output, either {CODE} or {CODE-1}.
      var chase_length = 0;
      var chase = chase_code;
      while (chase > clear_code) {
        chase = code_table[chase] >> 8;
        ++chase_length;
      }

      var k = chase;

      var op_end = op + chase_length + (chase_code !== code ? 1 : 0);
      if (op_end > output_length) {
        console.log("Warning, gif stream longer than expected.");
        return;
      }

      // Already have the first byte from the chase, might as well write it fast.
      output[op++] = k;

      op += chase_length;
      var b = op;  // Track pointer, writing backwards.

      if (chase_code !== code)  // The case of emitting {CODE-1} + k.
        output[op++] = k;

      chase = chase_code;
      while (chase_length--) {
        chase = code_table[chase];
        output[--b] = chase & 0xff;  // Write backwards.
        chase >>= 8;  // Pull down to the prefix code.
      }

      if (prev_code !== null && next_code < 4096) {
        code_table[next_code++] = prev_code << 8 | k;
        // TODO(deanm): Figure out this clearing vs code growth logic better.  I
        // have an feeling that it should just happen somewhere else, for now it
        // is awkward between when we grow past the max and then hit a clear code.
        // For now just check if we hit the max 12-bits (then a clear code should
        // follow, also of course encoded in 12-bits).
        if (next_code >= code_mask+1 && cur_code_size < 12) {
          ++cur_code_size;
          code_mask = code_mask << 1 | 1;
        }
      }

      prev_code = code;
    }

    if (op !== output_length) {
      console.log("Warning, gif stream shorter than expected.");
    }

    return output;
  }

  // CommonJS.
  try { GifWriter_1 = GifWriter; GifReader_1 = GifReader; } catch(e) {}

  /**
   * @module Image
   * @submodule Image
   * @for p5
   * @requires core
   */


  function image$1(p5, fn){
    /**
     * Creates a new <a href="#/p5.Image">p5.Image</a> object.
     *
     * `createImage()` uses the `width` and `height` parameters to set the new
     * <a href="#/p5.Image">p5.Image</a> object's dimensions in pixels. The new
     * <a href="#/p5.Image">p5.Image</a> can be modified by updating its
     * <a href="#/p5.Image/pixels">pixels</a> array or by calling its
     * <a href="#/p5.Image/get">get()</a> and
     * <a href="#/p5.Image/set">set()</a> methods. The
     * <a href="#/p5.Image/loadPixels">loadPixels()</a> method must be called
     * before reading or modifying pixel values. The
     * <a href="#/p5.Image/updatePixels">updatePixels()</a> method must be called
     * for updates to take effect.
     *
     * Note: The new <a href="#/p5.Image">p5.Image</a> object is transparent by
     * default.
     *
     * @method createImage
     * @param  {Integer} width  width in pixels.
     * @param  {Integer} height height in pixels.
     * @return {p5.Image}       new <a href="#/p5.Image">p5.Image</a> object.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Image object.
     *   let img = createImage(66, 66);
     *
     *   // Load the image's pixels into memory.
     *   img.loadPixels();
     *
     *   // Set all the image's pixels to black.
     *   for (let x = 0; x < img.width; x += 1) {
     *     for (let y = 0; y < img.height; y += 1) {
     *       img.set(x, y, 0);
     *     }
     *   }
     *
     *   // Update the image's pixel values.
     *   img.updatePixels();
     *
     *   // Draw the image.
     *   image(img, 17, 17);
     *
     *   describe('A black square drawn in the middle of a gray square.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Image object.
     *   let img = createImage(66, 66);
     *
     *   // Load the image's pixels into memory.
     *   img.loadPixels();
     *
     *   // Create a color gradient.
     *   for (let x = 0; x < img.width; x += 1) {
     *     for (let y = 0; y < img.height; y += 1) {
     *       // Calculate the transparency.
     *       let a = map(x, 0, img.width, 0, 255);
     *
     *       // Create a p5.Color object.
     *       let c = color(0, a);
     *
     *       // Set the pixel's color.
     *       img.set(x, y, c);
     *     }
     *   }
     *
     *   // Update the image's pixels.
     *   img.updatePixels();
     *
     *   // Display the image.
     *   image(img, 17, 17);
     *
     *   describe('A square with a horizontal color gradient that transitions from gray to black.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Image object.
     *   let img = createImage(66, 66);
     *
     *   // Load the pixels into memory.
     *   img.loadPixels();
     *   // Get the current pixel density.
     *   let d = pixelDensity();
     *
     *   // Calculate the pixel that is halfway through the image's pixel array.
     *   let halfImage = 4 * (d * img.width) * (d * img.height / 2);
     *
     *   // Set half of the image's pixels to black.
     *   for (let i = 0; i < halfImage; i += 4) {
     *     // Red.
     *     img.pixels[i] = 0;
     *     // Green.
     *     img.pixels[i + 1] = 0;
     *     // Blue.
     *     img.pixels[i + 2] = 0;
     *     // Alpha.
     *     img.pixels[i + 3] = 255;
     *   }
     *
     *   // Update the image's pixels.
     *   img.updatePixels();
     *
     *   // Display the image.
     *   image(img, 17, 17);
     *
     *   describe('A black square drawn in the middle of a gray square.');
     * }
     */
    fn.createImage = function(width, height) {
      // p5._validateParameters('createImage', arguments);
      return new p5.Image(width, height);
    };

    /**
     * Saves the current canvas as an image.
     *
     * By default, `saveCanvas()` saves the canvas as a PNG image called
     * `untitled.png`.
     *
     * The first parameter, `filename`, is optional. It's a string that sets the
     * file's name. If a file extension is included, as in
     * `saveCanvas('drawing.png')`, then the image will be saved using that
     * format.
     *
     * The second parameter, `extension`, is also optional. It sets the files format.
     * Either `'png'`, `'webp'`, or `'jpg'` can be used. For example, `saveCanvas('drawing', 'jpg')`
     * saves the canvas to a file called `drawing.jpg`.
     *
     * Note: The browser will either save the file immediately or prompt the user
     * with a dialogue window.
     *
     *  @method saveCanvas
     *  @param  {p5.Framebuffer|p5.Element|HTMLCanvasElement} selectedCanvas   reference to a
     *                                                          specific HTML5 canvas element.
     *  @param  {String} [filename]  file name. Defaults to 'untitled'.
     *  @param  {String} [extension] file extension, either 'png', 'webp', or 'jpg'. Defaults to 'png'.
     *
     * @example
     * // META:norender
     * function setup() {
     *   createCanvas(100, 100);
     *   background(255);
     *
     *   // Save the canvas to 'untitled.png'.
     *   saveCanvas();
     *
     *   describe('A white square.');
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(255);
     *
     *   // Save the canvas to 'myCanvas.jpg'.
     *   saveCanvas('myCanvas.jpg');
     *
     *   describe('A white square.');
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(255);
     *
     *   // Save the canvas to 'myCanvas.jpg'.
     *   saveCanvas('myCanvas', 'jpg');
     *
     *   describe('A white square.');
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   let cnv = createCanvas(100, 100);
     *
     *   background(255);
     *
     *   // Save the canvas to 'untitled.png'.
     *   saveCanvas(cnv);
     *
     *   describe('A white square.');
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   let cnv = createCanvas(100, 100);
     *
     *   background(255);
     *
     *   // Save the canvas to 'myCanvas.jpg'.
     *   saveCanvas(cnv, 'myCanvas.jpg');
     *
     *   describe('A white square.');
     * }
     *
     * @example
     * // META:norender
     * function setup() {
     *   let cnv = createCanvas(100, 100);
     *
     *   background(255);
     *
     *   // Save the canvas to 'myCanvas.jpg'.
     *   saveCanvas(cnv, 'myCanvas', 'jpg');
     *
     *   describe('A white square.');
     * }
     */
    /**
     *  @method saveCanvas
     *  @param  {String} [filename]
     *  @param  {String} [extension]
     */
    fn.saveCanvas = function(...args) {
      // copy arguments to array
      let htmlCanvas, filename, extension, temporaryGraphics;

      if (args[0] instanceof HTMLCanvasElement) {
        htmlCanvas = args[0];
        args.shift();
      } else if (args[0] instanceof Element) {
        htmlCanvas = args[0].elt;
        args.shift();
      } else if (args[0] instanceof Framebuffer$1) {
        const framebuffer = args[0];
        temporaryGraphics = this.createGraphics(framebuffer.width,
          framebuffer.height);
        temporaryGraphics.pixelDensity(framebuffer.pixelDensity());
        framebuffer.loadPixels();
        temporaryGraphics.loadPixels();
        temporaryGraphics.pixels.set(framebuffer.pixels);
        temporaryGraphics.updatePixels();

        htmlCanvas = temporaryGraphics._renderer.canvas;
        args.shift();
      } else {
        htmlCanvas = this._curElement && this._curElement.elt;
      }

      if (args.length >= 1) {
        filename = args[0];
      }
      if (args.length >= 2) {
        extension = args[1];
      }

      extension =
        extension ||
        fn._checkFileExtension(filename, extension)[1] ||
        'png';

      let mimeType;
      switch (extension) {
        default:
          //case 'png':
          mimeType = 'image/png';
          break;
        case 'webp':
          mimeType = 'image/webp';
          break;
        case 'jpeg':
        case 'jpg':
          mimeType = 'image/jpeg';
          break;
      }

      htmlCanvas.toBlob(blob => {
        fn.downloadFile(blob, filename, extension);
        if(temporaryGraphics) temporaryGraphics.remove();
      }, mimeType);
    };

    // this is the old saveGif, left here for compatibility purposes
    // the only place I found it being used was on image/p5.Image.js, on the
    // save function. that has been changed to use this function.
    fn.encodeAndDownloadGif = function(pImg, filename) {
      const props = pImg.gifProperties;

      //convert loopLimit back into Netscape Block formatting
      let loopLimit = props.loopLimit;
      if (loopLimit === 1) {
        loopLimit = null;
      } else if (loopLimit === null) {
        loopLimit = 0;
      }
      const buffer = new Uint8Array(pImg.width * pImg.height * props.numFrames);

      const allFramesPixelColors = [];

      // Used to determine the occurrence of unique palettes and the frames
      // which use them
      const paletteFreqsAndFrames = {};

      // Pass 1:
      //loop over frames and get the frequency of each palette
      for (let i = 0; i < props.numFrames; i++) {
        const paletteSet = new Set();
        const data = props.frames[i].image.data;
        const dataLength = data.length;
        // The color for each pixel in this frame ( for easier lookup later )
        const pixelColors = new Uint32Array(pImg.width * pImg.height);
        for (let j = 0, k = 0; j < dataLength; j += 4, k++) {
          const r = data[j + 0];
          const g = data[j + 1];
          const b = data[j + 2];
          const color = (r << 16) | (g << 8) | (b << 0);
          paletteSet.add(color);

          // What color does this pixel have in this frame ?
          pixelColors[k] = color;
        }

        // A way to put use the entire palette as an object key
        const paletteStr = [...paletteSet].sort().toString();
        if (paletteFreqsAndFrames[paletteStr] === undefined) {
          paletteFreqsAndFrames[paletteStr] = { freq: 1, frames: [i] };
        } else {
          paletteFreqsAndFrames[paletteStr].freq += 1;
          paletteFreqsAndFrames[paletteStr].frames.push(i);
        }

        allFramesPixelColors.push(pixelColors);
      }

      let framesUsingGlobalPalette = [];

      // Now to build the global palette
      // Sort all the unique palettes in descending order of their occurrence
      const palettesSortedByFreq = Object.keys(paletteFreqsAndFrames)
        .sort(function(
          a,
          b
        ) {
          return paletteFreqsAndFrames[b].freq - paletteFreqsAndFrames[a].freq;
        });

      // The initial global palette is the one with the most occurrence
      const globalPalette = palettesSortedByFreq[0]
        .split(',')
        .map(a => parseInt(a));

      framesUsingGlobalPalette = framesUsingGlobalPalette.concat(
        paletteFreqsAndFrames[globalPalette].frames
      );

      const globalPaletteSet = new Set(globalPalette);

      // Build a more complete global palette
      // Iterate over the remaining palettes in the order of
      // their occurrence and see if the colors in this palette which are
      // not in the global palette can be added there, while keeping the length
      // of the global palette <= 256
      for (let i = 1; i < palettesSortedByFreq.length; i++) {
        const palette = palettesSortedByFreq[i].split(',').map(a => parseInt(a));

        const difference = palette.filter(x => !globalPaletteSet.has(x));
        if (globalPalette.length + difference.length <= 256) {
          for (let j = 0; j < difference.length; j++) {
            globalPalette.push(difference[j]);
            globalPaletteSet.add(difference[j]);
          }

          // All frames using this palette now use the global palette
          framesUsingGlobalPalette = framesUsingGlobalPalette.concat(
            paletteFreqsAndFrames[palettesSortedByFreq[i]].frames
          );
        }
      }

      framesUsingGlobalPalette = new Set(framesUsingGlobalPalette);

      // Build a lookup table of the index of each color in the global palette
      // Maps a color to its index
      const globalIndicesLookup = {};
      for (let i = 0; i < globalPalette.length; i++) {
        if (!globalIndicesLookup[globalPalette[i]]) {
          globalIndicesLookup[globalPalette[i]] = i;
        }
      }

      // force palette to be power of 2
      let powof2 = 1;
      while (powof2 < globalPalette.length) {
        powof2 <<= 1;
      }
      globalPalette.length = powof2;

      // global opts
      const opts = {
        loop: loopLimit,
        palette: new Uint32Array(globalPalette)
      };
      const gifWriter = new GifWriter_1(
        buffer,
        pImg.width,
        pImg.height,
        opts
      );
      let previousFrame = {};

      // Pass 2
      // Determine if the frame needs a local palette
      // Also apply transparency optimization. This function will often blow up
      // the size of a GIF if not for transparency. If a pixel in one frame has
      // the same color in the previous frame, that pixel can be marked as
      // transparent. We decide one particular color as transparent and make all
      // transparent pixels take this color. This helps in later in compression.
      for (let i = 0; i < props.numFrames; i++) {
        const localPaletteRequired = !framesUsingGlobalPalette.has(i);
        const palette = localPaletteRequired ? [] : globalPalette;
        const pixelPaletteIndex = new Uint8Array(pImg.width * pImg.height);

        // Lookup table mapping color to its indices
        const colorIndicesLookup = {};

        // All the colors that cannot be marked transparent in this frame
        const cannotBeTransparent = new Set();

        allFramesPixelColors[i].forEach((color, k) => {
          if (localPaletteRequired) {
            if (colorIndicesLookup[color] === undefined) {
              colorIndicesLookup[color] = palette.length;
              palette.push(color);
            }
            pixelPaletteIndex[k] = colorIndicesLookup[color];
          } else {
            pixelPaletteIndex[k] = globalIndicesLookup[color];
          }

          if (i > 0) {
            // If even one pixel of this color has changed in this frame
            // from the previous frame, we cannot mark it as transparent
            if (allFramesPixelColors[i - 1][k] !== color) {
              cannotBeTransparent.add(color);
            }
          }
        });

        const frameOpts = {};

        // Transparency optimization
        const canBeTransparent = palette.filter(a => !cannotBeTransparent.has(a));
        if (canBeTransparent.length > 0) {
          // Select a color to mark as transparent
          const transparent = canBeTransparent[0];
          const transparentIndex = localPaletteRequired
            ? colorIndicesLookup[transparent]
            : globalIndicesLookup[transparent];
          if (i > 0) {
            for (let k = 0; k < allFramesPixelColors[i].length; k++) {
              // If this pixel in this frame has the same color in previous frame
              if (allFramesPixelColors[i - 1][k] === allFramesPixelColors[i][k]) {
                pixelPaletteIndex[k] = transparentIndex;
              }
            }
            frameOpts.transparent = transparentIndex;
            // If this frame has any transparency, do not dispose the previous frame
            previousFrame.frameOpts.disposal = 1;
          }
        }
        frameOpts.delay = props.frames[i].delay / 10; // Move timing back into GIF formatting
        if (localPaletteRequired) {
          // force palette to be power of 2
          let powof2 = 1;
          while (powof2 < palette.length) {
            powof2 <<= 1;
          }
          palette.length = powof2;
          frameOpts.palette = new Uint32Array(palette);
        }
        if (i > 0) {
          // add the frame that came before the current one
          gifWriter.addFrame(
            0,
            0,
            pImg.width,
            pImg.height,
            previousFrame.pixelPaletteIndex,
            previousFrame.frameOpts
          );
        }
        // previous frame object should now have details of this frame
        previousFrame = {
          pixelPaletteIndex,
          frameOpts
        };
      }

      previousFrame.frameOpts.disposal = 1;
      // add the last frame
      gifWriter.addFrame(
        0,
        0,
        pImg.width,
        pImg.height,
        previousFrame.pixelPaletteIndex,
        previousFrame.frameOpts
      );

      const extension = 'gif';
      const blob = new Blob([buffer.slice(0, gifWriter.end())], {
        type: 'image/gif'
      });
      fn.downloadFile(blob, filename, extension);
    };

    /**
     * Captures a sequence of frames from the canvas that can be saved as images.
     *
     * `saveFrames()` creates an array of frame objects. Each frame is stored as
     * an object with its file type, file name, and image data as a string. For
     * example, the first saved frame might have the following properties:
     *
     * `{ ext: 'png', filenmame: 'frame0', imageData: 'data:image/octet-stream;base64, abc123' }`.
     *
     * The first parameter, `filename`, sets the prefix for the file names. For
     * example, setting the prefix to `'frame'` would generate the image files
     * `frame0.png`, `frame1.png`, and so on.
     *
     * The second parameter, `extension`, sets the file type to either `'png'` or
     * `'jpg'`.
     *
     * The third parameter, `duration`, sets the duration to record in seconds.
     * The maximum duration is 15 seconds.
     *
     * The fourth parameter, `framerate`, sets the number of frames to record per
     * second. The maximum frame rate value is 22. Limits are placed on `duration`
     * and `framerate` to avoid using too much memory. Recording large canvases
     * can easily crash sketches or even web browsers.
     *
     * The fifth parameter, `callback`, is optional. If a function is passed,
     * image files won't be saved by default. The callback function can be used
     * to process an array containing the data for each captured frame. The array
     * of image data contains a sequence of objects with three properties for each
     * frame: `imageData`, `filename`, and `extension`.
     *
     * Note: Frames are downloaded as individual image files by default.
     *
     * @method saveFrames
     * @param  {String}   filename  prefix of file name.
     * @param  {String}   extension file extension, either 'jpg' or 'png'.
     * @param  {Number}   duration  duration in seconds to record. This parameter will be constrained to be less or equal to 15.
     * @param  {Number}   framerate number of frames to save per second. This parameter will be constrained to be less or equal to 22.
     * @param  {function(Array)} [callback] callback function that will be executed
     *                                to handle the image data. This function
     *                                should accept an array as argument. The
     *                                array will contain the specified number of
     *                                frames of objects. Each object has three
     *                                properties: `imageData`, `filename`, and `extension`.
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A square repeatedly changes color from blue to pink.');
     * }
     *
     * function draw() {
     *   let r = frameCount % 255;
     *   let g = 50;
     *   let b = 100;
     *   background(r, g, b);
     * }
     *
     * // Save the frames when the user presses the 's' key.
     * function keyPressed() {
     *   if (key === 's') {
     *     saveFrames('frame', 'png', 1, 5);
     *   }
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A square repeatedly changes color from blue to pink.');
     * }
     *
     * function draw() {
     *   let r = frameCount % 255;
     *   let g = 50;
     *   let b = 100;
     *   background(r, g, b);
     * }
     *
     * // Print 5 frames when the user presses the mouse.
     * function mousePressed() {
     *   saveFrames('frame', 'png', 1, 5, printFrames);
     * }
     *
     * // Prints an array of objects containing raw image data, filenames, and extensions.
     * function printFrames(frames) {
     *   for (let frame of frames) {
     *     print(frame);
     *   }
     * }
     */
    fn.saveFrames = function(fName, ext, _duration, _fps, callback) {
      // p5._validateParameters('saveFrames', arguments);
      let duration = _duration || 3;
      duration = Math.max(Math.min(duration, 15), 0);
      duration = duration * 1000;
      let fps = _fps || 15;
      fps = Math.max(Math.min(fps, 22), 0);
      let count = 0;

      const makeFrame = fn._makeFrame;
      const cnv = this._curElement.elt;
      let frames = [];
      const frameFactory = setInterval(() => {
        frames.push(makeFrame(fName + count, ext, cnv));
        count++;
      }, 1000 / fps);

      setTimeout(() => {
        clearInterval(frameFactory);
        if (callback) {
          callback(frames);
        } else {
          for (const f of frames) {
            fn.downloadFile(f.imageData, f.filename, f.ext);
          }
        }
        frames = []; // clear frames
      }, duration + 0.01);
    };

    fn._makeFrame = function(filename, extension, _cnv) {
      let cnv;
      if (this) {
        cnv = this._curElement.elt;
      } else {
        cnv = _cnv;
      }
      let mimeType;
      if (!extension) {
        extension = 'png';
        mimeType = 'image/png';
      } else {
        switch (extension.toLowerCase()) {
          case 'png':
            mimeType = 'image/png';
            break;
          case 'jpeg':
            mimeType = 'image/jpeg';
            break;
          case 'jpg':
            mimeType = 'image/jpeg';
            break;
          default:
            mimeType = 'image/png';
            break;
        }
      }
      const downloadMime = 'image/octet-stream';
      let imageData = cnv.toDataURL(mimeType);
      imageData = imageData.replace(mimeType, downloadMime);

      const thisFrame = {};
      thisFrame.imageData = imageData;
      thisFrame.filename = filename;
      thisFrame.ext = extension;
      return thisFrame;
    };
  }

  if(typeof p5 !== 'undefined'){
    image$1(p5, p5.prototype);
  }

  /*
  The MIT License (MIT)

  Copyright (c) 2019 Evan Plaice <evanplaice@gmail.com>

  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.
  */
  function parse$3 (csv, options, reviver = v => v) {
    const ctx = Object.create(null);
    ctx.options = options || {};
    ctx.reviver = reviver;
    ctx.value = '';
    ctx.entry = [];
    ctx.output = [];
    ctx.col = 1;
    ctx.row = 1;

    ctx.options.delimiter = ctx.options.delimiter === undefined ? '"' : options.delimiter;
    if(ctx.options.delimiter.length > 1 || ctx.options.delimiter.length === 0)
      throw Error(`CSVError: delimiter must be one character [${ctx.options.separator}]`);

    ctx.options.separator = ctx.options.separator === undefined ? ',' : options.separator;
    if(ctx.options.separator.length > 1 || ctx.options.separator.length === 0)
      throw Error(`CSVError: separator must be one character [${ctx.options.separator}]`);

    const lexer = new RegExp(`${escapeRegExp(ctx.options.delimiter)}|${escapeRegExp(ctx.options.separator)}|\r\n|\n|\r|[^${escapeRegExp(ctx.options.delimiter)}${escapeRegExp(ctx.options.separator)}\r\n]+`, 'y');
    const isNewline = /^(\r\n|\n|\r)$/;

    let matches = [];
    let match = '';
    let state = 0;

    while ((matches = lexer.exec(csv)) !== null) {
      match = matches[0];

      switch (state) {
        case 0: // start of entry
          switch (true) {
            case match === ctx.options.delimiter:
              state = 3;
              break;
            case match === ctx.options.separator:
              state = 0;
              valueEnd(ctx);
              break;
            case isNewline.test(match):
              state = 0;
              valueEnd(ctx);
              entryEnd(ctx);
              break;
            default:
              ctx.value += match;
              state = 2;
              break;
          }
          break;
        case 2: // un-delimited input
          switch (true) {
            case match === ctx.options.separator:
              state = 0;
              valueEnd(ctx);
              break;
            case isNewline.test(match):
              state = 0;
              valueEnd(ctx);
              entryEnd(ctx);
              break;
            default:
              state = 4;
              throw Error(`CSVError: Illegal state [row:${ctx.row}, col:${ctx.col}]`);
          }
          break;
        case 3: // delimited input
          switch (true) {
            case match === ctx.options.delimiter:
              state = 4;
              break;
            default:
              state = 3;
              ctx.value += match;
              break;
          }
          break;
        case 4: // escaped or closing delimiter
          switch (true) {
            case match === ctx.options.delimiter:
              state = 3;
              ctx.value += match;
              break;
            case match === ctx.options.separator:
              state = 0;
              valueEnd(ctx);
              break;
            case isNewline.test(match):
              state = 0;
              valueEnd(ctx);
              entryEnd(ctx);
              break;
            default:
              throw Error(`CSVError: Illegal state [row:${ctx.row}, col:${ctx.col}]`);
          }
          break;
      }
    }

    // flush the last value
    if (ctx.entry.length !== 0) {
      valueEnd(ctx);
      entryEnd(ctx);
    }

    return ctx.output;
  }

  function stringify (array, options = {}, replacer = v => v) {
    const ctx = Object.create(null);
    ctx.options = options;
    ctx.options.eof = ctx.options.eof !== undefined ? ctx.options.eof : true;
    ctx.row = 1;
    ctx.col = 1;
    ctx.output = '';

    ctx.options.delimiter = ctx.options.delimiter === undefined ? '"' : options.delimiter;
    if(ctx.options.delimiter.length > 1 || ctx.options.delimiter.length === 0)
      throw Error(`CSVError: delimiter must be one character [${ctx.options.separator}]`);

    ctx.options.separator = ctx.options.separator === undefined ? ',' : options.separator;
    if(ctx.options.separator.length > 1 || ctx.options.separator.length === 0)
      throw Error(`CSVError: separator must be one character [${ctx.options.separator}]`);

    const needsDelimiters = new RegExp(`${escapeRegExp(ctx.options.delimiter)}|${escapeRegExp(ctx.options.separator)}|\r\n|\n|\r`);

    array.forEach((row, rIdx) => {
      let entry = '';
      ctx.col = 1;
      row.forEach((col, cIdx) => {
        if (typeof col === 'string') {
          col = col.replace(new RegExp(ctx.options.delimiter, 'g'), `${ctx.options.delimiter}${ctx.options.delimiter}`);
          col = needsDelimiters.test(col) ? `${ctx.options.delimiter}${col}${ctx.options.delimiter}` : col;
        }
        entry += replacer(col, ctx.row, ctx.col);
        if (cIdx !== row.length - 1) {
          entry += ctx.options.separator;
        }
        ctx.col++;
      });
      switch (true) {
        case ctx.options.eof:
        case !ctx.options.eof && rIdx !== array.length - 1:
          ctx.output += `${entry}\n`;
          break;
        default:
          ctx.output += `${entry}`;
          break;
      }
      ctx.row++;
    });

    return ctx.output;
  }

  function valueEnd (ctx) {
    const value = ctx.options.typed ? inferType(ctx.value) : ctx.value;
    ctx.entry.push(ctx.reviver(value, ctx.row, ctx.col));
    ctx.value = '';
    ctx.col++;
  }

  function entryEnd (ctx) {
    ctx.output.push(ctx.entry);
    ctx.entry = [];
    ctx.row++;
    ctx.col = 1;
  }

  function inferType (value) {
    const isNumber = /.\./;

    switch (true) {
      case value === 'true':
      case value === 'false':
        return value === 'true';
      case isNumber.test(value):
        return parseFloat(value);
      case isFinite(value):
        return parseInt(value);
      default:
        return value;
    }
  }

  function escapeRegExp(str) {
    return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
  }

  /**
   * @module IO
   * @submodule Input
   * @for p5
   * @requires core
   */


  class HTTPError extends Error {
    status;
    response;
    ok;
  }

  async function request(path, type){
    try {
      const res = await fetch(path);

      if (res.ok) {
        let data;
        switch(type) {
          case 'json':
            data = await res.json();
            break;
          case 'text':
            data = await res.text();
            break;
          case 'arrayBuffer':
            data = await res.arrayBuffer();
            break;
          case 'blob':
            data = await res.blob();
            break;
          case 'bytes':
            // TODO: Chrome does not implement res.bytes() yet
            if(res.bytes){
              data = await res.bytes();
            }else {
              const d = await res.arrayBuffer();
              data = new Uint8Array(d);
            }
            break;
          default:
            throw new Error('Unsupported response type');
        }

        return { data, headers: res.headers };

      } else {
        const err = new HTTPError(res.statusText);
        err.status = res.status;
        err.response = res;
        err.ok = false;

        throw err;
      }

    } catch(err) {
      // Handle both fetch error and HTTP error
      if (err instanceof TypeError) {
        console.log('You may have encountered a CORS error');
      } else if (err instanceof HTTPError) {
        console.log('You have encountered a HTTP error');
      } else if (err instanceof SyntaxError) {
        console.log('There is an error parsing the response to requested data structure');
      }

      throw err;
    }
  }

  function files(p5, fn){
    /**
     * Loads a JSON file to create an `Object`.
     *
     * JavaScript Object Notation
     * (<a href="https://developer.mozilla.org/en-US/docs/Glossary/JSON" target="_blank">JSON</a>)
     * is a standard format for sending data between applications. The format is
     * based on JavaScript objects which have keys and values. JSON files store
     * data in an object with strings as keys. Values can be strings, numbers,
     * Booleans, arrays, `null`, or other objects.
     *
     * The first parameter, `path`, is a string with the path to the file.
     * Paths to local files should be relative, as in
     * `loadJSON('assets/data.json')`. URLs such as
     * `'https://example.com/data.json'` may be blocked due to browser security.
     * The `path` parameter can also be defined as a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
     * object for more advanced usage.
     *
     * The second parameter, `successCallback`, is optional. If a function is
     * passed, as in `loadJSON('assets/data.json', handleData)`, then the
     * `handleData()` function will be called once the data loads. The object
     * created from the JSON data will be passed to `handleData()` as its only argument.
     * The return value of the `handleData()` function will be used as the final return
     * value of `loadJSON('assets/data.json', handleData)`.
     *
     * The third parameter, `failureCallback`, is also optional. If a function is
     * passed, as in `loadJSON('assets/data.json', handleData, handleFailure)`,
     * then the `handleFailure()` function will be called if an error occurs while
     * loading. The `Error` object will be passed to `handleFailure()` as its only
     * argument. The return value of the `handleFailure()` function will be used as the
     * final return value of `loadJSON('assets/data.json', handleData, handleFailure)`.
     *
     * This function returns a `Promise` and should be used in an `async` setup with
     * `await`. See the examples for the usage syntax.
     *
     * @method loadJSON
     * @param  {String|Request} path path of the JSON file to be loaded.
     * @param  {Function} [successCallback] function to call once the data is loaded. Will be passed the object.
     * @param  {Function} [errorCallback] function to call if the data fails to load. Will be passed an `Error` event object.
     * @return {Promise<Object>} object containing the loaded data.
     *
     * @example
     * let myData;
     *
     * async function setup() {
     *   myData = await loadJSON('assets/data.json');
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the circle.
     *   fill(myData.color);
     *   noStroke();
     *
     *   // Draw the circle.
     *   circle(myData.x, myData.y, myData.d);
     *
     *   describe('A pink circle on a gray background.');
     * }
     *
     * @example
     * let myData;
     *
     * async function setup() {
     *   myData = await loadJSON('assets/data.json');
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object and make it transparent.
     *   let c = color(myData.color);
     *   c.setAlpha(80);
     *
     *   // Style the circles.
     *   fill(c);
     *   noStroke();
     *
     *   // Iterate over the myData.bubbles array.
     *   for (let b of myData.bubbles) {
     *     // Draw a circle for each bubble.
     *     circle(b.x, b.y, b.d);
     *   }
     *
     *   describe('Several pink bubbles floating in a blue sky.');
     * }
     *
     * @example
     * let myData;
     *
     * async function setup() {
     *   myData = await loadJSON('https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson');
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get data about the most recent earthquake.
     *   let quake = myData.features[0].properties;
     *
     *   // Draw a circle based on the earthquake's magnitude.
     *   circle(50, 50, quake.mag * 10);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(11);
     *
     *   // Display the earthquake's location.
     *   text(quake.place, 5, 80, 100);
     *
     *   describe(`A white circle on a gray background. The text "${quake.place}" is written beneath the circle.`);
     * }
     *
     * @example
     * let bigQuake;
     *
     * // Load the GeoJSON and preprocess it.
     * async function setup() {
     *   await loadJSON(
     *     'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson',
     *     handleData
     *   );
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Draw a circle based on the earthquake's magnitude.
     *   circle(50, 50, bigQuake.mag * 10);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(11);
     *
     *   // Display the earthquake's location.
     *   text(bigQuake.place, 5, 80, 100);
     *
     *   describe(`A white circle on a gray background. The text "${bigQuake.place}" is written beneath the circle.`);
     * }
     *
     * // Find the biggest recent earthquake.
     * function handleData(data) {
     *   let maxMag = 0;
     *   // Iterate over the earthquakes array.
     *   for (let quake of data.features) {
     *     // Reassign bigQuake if a larger
     *     // magnitude quake is found.
     *     if (quake.properties.mag > maxMag) {
     *       bigQuake = quake.properties;
     *     }
     *   }
     * }
     *
     * @example
     * let bigQuake;
     *
     * // Load the GeoJSON and preprocess it.
     * async function setup() {
     *   await loadJSON(
     *     'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson',
     *     handleData,
     *     handleError
     *   );
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Draw a circle based on the earthquake's magnitude.
     *   circle(50, 50, bigQuake.mag * 10);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(11);
     *
     *   // Display the earthquake's location.
     *   text(bigQuake.place, 5, 80, 100);
     *
     *   describe(`A white circle on a gray background. The text "${bigQuake.place}" is written beneath the circle.`);
     * }
     *
     * // Find the biggest recent earthquake.
     * function handleData(data) {
     *   let maxMag = 0;
     *   // Iterate over the earthquakes array.
     *   for (let quake of data.features) {
     *     // Reassign bigQuake if a larger
     *     // magnitude quake is found.
     *     if (quake.properties.mag > maxMag) {
     *       bigQuake = quake.properties;
     *     }
     *   }
     * }
     *
     * // Log any errors to the console.
     * function handleError(error) {
     *   console.log('Oops!', error);
     * }
     */
    fn.loadJSON = async function (path, successCallback, errorCallback) {
      // p5._validateParameters('loadJSON', arguments);

      try{
        const { data } = await request(path, 'json');
        if (successCallback) return successCallback(data);
        return data;
      } catch(err) {
        p5._friendlyFileLoadError(5, path);
        if(errorCallback) {
          return errorCallback(err);
        } else {
          throw err;
        }
      }
    };

    /**
     * Loads a text file to create an `Array`.
     *
     * The first parameter, `path`, is always a string with the path to the file.
     * Paths to local files should be relative, as in
     * `loadStrings('assets/data.txt')`. URLs such as
     * `'https://example.com/data.txt'` may be blocked due to browser security.
     * The `path` parameter can also be defined as a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
     * object for more advanced usage.
     *
     * The second parameter, `successCallback`, is optional. If a function is
     * passed, as in `loadStrings('assets/data.txt', handleData)`, then the
     * `handleData()` function will be called once the data loads. The array
     * created from the text data will be passed to `handleData()` as its only
     * argument. The return value of the `handleData()` function will be used as
     * the final return value of `loadStrings('assets/data.txt', handleData)`.
     *
     * The third parameter, `failureCallback`, is also optional. If a function is
     * passed, as in `loadStrings('assets/data.txt', handleData, handleFailure)`,
     * then the `handleFailure()` function will be called if an error occurs while
     * loading. The `Error` object will be passed to `handleFailure()` as its only
     * argument. The return value of the `handleFailure()` function will be used as
     * the final return value of `loadStrings('assets/data.txt', handleData, handleFailure)`.
     *
     * This function returns a `Promise` and should be used in an `async` setup with
     * `await`. See the examples for the usage syntax.
     *
     * @method loadStrings
     * @param  {String|Request} path path of the text file to be loaded.
     * @param  {Function} [successCallback] function to call once the data is
     *                                      loaded. Will be passed the array.
     * @param  {Function} [errorCallback] function to call if the data fails to
     *                                    load. Will be passed an `Error` event
     *                                    object.
     * @return {Promise<String[]>} new array containing the loaded text.
     *
     * @example
     * let myData;
     *
     * async function setup() {
     *   myData = await loadStrings('assets/test.txt');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Select a random line from the text.
     *   let phrase = random(myData);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(12);
     *
     *   // Display the text.
     *   text(phrase, 10, 50, 90);
     *
     *   describe(`The text "${phrase}" written in black on a gray background.`);
     * }
     *
     * @example
     * let lastLine;
     *
     * // Load the text and preprocess it.
     * async function setup() {
     *   await loadStrings('assets/test.txt', handleData);
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(12);
     *
     *   // Display the text.
     *   text(lastLine, 10, 50, 90);
     *
     *   describe('The text "I talk like an orange" written in black on a gray background.');
     * }
     *
     * // Select the last line from the text.
     * function handleData(data) {
     *   lastLine = data[data.length - 1];
     * }
     *
     * @example
     * let lastLine;
     *
     * // Load the text and preprocess it.
     * async function setup() {
     *   await loadStrings('assets/test.txt', handleData, handleError);
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(12);
     *
     *   // Display the text.
     *   text(lastLine, 10, 50, 90);
     *
     *   describe('The text "I talk like an orange" written in black on a gray background.');
     * }
     *
     * // Select the last line from the text.
     * function handleData(data) {
     *   lastLine = data[data.length - 1];
     * }
     *
     * // Log any errors to the console.
     * function handleError(error) {
     *   console.error('Oops!', error);
     * }
     */
    fn.loadStrings = async function (path, successCallback, errorCallback) {
      // p5._validateParameters('loadStrings', arguments);

      try{
        let { data } = await request(path, 'text');
        data = data.split(/\r?\n/);

        if (successCallback) return successCallback(data);
        return data;
      } catch(err) {
        p5._friendlyFileLoadError(3, path);
        if(errorCallback) {
          return errorCallback(err);
        } else {
          throw err;
        }
      }
    };

    /**
     * Reads the contents of a file or URL and creates a <a href="#/p5.Table">p5.Table</a> object with
     * its values. If a file is specified, it must be located in the sketch's
     * "data" folder. The filename parameter can also be a URL to a file found
     * online. By default, the file is assumed to be comma-separated (in CSV
     * format). Table only looks for a header row if the 'header' option is
     * included.
     *
     * This function returns a `Promise` and should be used in an `async` setup with
     * `await`. See the examples for the usage syntax.
     *
     * All files loaded and saved use UTF-8 encoding. This method is suitable for fetching files up to size of 64MB.
     *
     * @method loadTable
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param  {String|Request} filename    name of the file or URL to load
     * @param  {String}         [separator] the separator character used by the file, defaults to `','`
     * @param  {String}         [header]    "header" to indicate table has header row
     * @param  {Function}       [callback]  function to be executed after
     *                                      <a href="#/p5/loadTable">loadTable()</a> completes. On success, the
     *                                      <a href="#/p5.Table">Table</a> object is passed in as the
     *                                      first argument.
     * @param  {Function}  [errorCallback]  function to be executed if
     *                                      there is an error, response is passed
     *                                      in as first argument
     * @return {Promise<Object>}            <a href="#/p5.Table">Table</a> object containing data
     *
     * @example
     * // META:norender
     * let table;
     *
     * async function setup() {
     *   // Create a 200x200 canvas
     *   createCanvas(200, 200);
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Get the second row (index 1)
     *   let row = table.getRow(1);
     *
     *   // Set text properties
     *   fill(0);       // Set text color to black
     *   textSize(16);  // Adjust text size as needed
     *
     *   // Display each column value in the row on the canvas.
     *   // Using an offset for y-position so each value appears on a new line.
     *   for (let c = 0; c < table.getColumnCount(); c++) {
     *     text(row.getString(c), 10, 30 + c * 20);
     *   }
     * }
     */
    fn.loadTable = async function (
      path,
      separator,
      header,
      successCallback,
      errorCallback
    ) {
      if(typeof arguments[arguments.length-1] === 'function'){
        if(typeof arguments[arguments.length-2] === 'function'){
          successCallback = arguments[arguments.length-2];
          errorCallback = arguments[arguments.length-1];
        }else {
          successCallback = arguments[arguments.length-1];
        }
      }

      if(typeof separator !== 'string') separator = ',';
      if(typeof header === 'function') header = false;

      try{
        let { data } = await request(path, 'text');

        let ret = new p5.Table();
        data = parse$3(data, {
          separator
        });

        if(header){
          ret.columns = data.shift();
        }else {
          ret.columns = Array(data[0].length).fill(null);
        }

        data.forEach(line => {
          const row = new p5.TableRow(line);
          ret.addRow(row);
        });

        if (successCallback) {
          return successCallback(ret);
        } else {
          return ret;
        }
      } catch(err) {
        p5._friendlyFileLoadError(2, path);
        if(errorCallback) {
          return errorCallback(err);
        } else {
          throw err;
        }
      }
    };

    /**
     * Loads an XML file to create a <a href="#/p5.XML">p5.XML</a> object.
     *
     * Extensible Markup Language
     * (<a href="https://developer.mozilla.org/en-US/docs/Web/XML/XML_introduction" target="_blank">XML</a>)
     * is a standard format for sending data between applications. Like HTML, the
     * XML format is based on tags and attributes, as in
     * `&lt;time units="s"&gt;1234&lt;/time&gt;`.
     *
     * The first parameter, `path`, is always a string with the path to the file.
     * Paths to local files should be relative, as in
     * `loadXML('assets/data.xml')`. URLs such as `'https://example.com/data.xml'`
     * may be blocked due to browser security. The `path` parameter can also be defined
     * as a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
     * object for more advanced usage.
     *
     * The second parameter, `successCallback`, is optional. If a function is
     * passed, as in `loadXML('assets/data.xml', handleData)`, then the
     * `handleData()` function will be called once the data loads. The
     * <a href="#/p5.XML">p5.XML</a> object created from the data will be passed
     * to `handleData()` as its only argument. The return value of the `handleData()`
     * function will be used as the final return value of `loadXML('assets/data.xml', handleData)`.
     *
     * The third parameter, `failureCallback`, is also optional. If a function is
     * passed, as in `loadXML('assets/data.xml', handleData, handleFailure)`, then
     * the `handleFailure()` function will be called if an error occurs while
     * loading. The `Error` object will be passed to `handleFailure()` as its only
     * argument. The return value of the `handleFailure()` function will be used as the
     * final return value of `loadXML('assets/data.xml', handleData, handleFailure)`.
     *
     * This function returns a `Promise` and should be used in an `async` setup with
     * `await`. See the examples for the usage syntax.
     *
     * @method loadXML
     * @param  {String|Request} path        path of the XML file to be loaded.
     * @param  {Function} [successCallback] function to call once the data is
     *                                      loaded. Will be passed the
     *                                      <a href="#/p5.XML">p5.XML</a> object.
     * @param  {Function} [errorCallback] function to call if the data fails to
     *                                    load. Will be passed an `Error` event
     *                                    object.
     * @return {Promise<p5.XML>} XML data loaded into a <a href="#/p5.XML">p5.XML</a>
     *                  object.
     *
     * @example
     * let myXML;
     *
     * // Load the XML and create a p5.XML object.
     * async function setup() {
     *   myXML = await loadXML('assets/animals.xml');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get an array with all mammal tags.
     *   let mammals = myXML.getChildren('mammal');
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(14);
     *
     *   // Iterate over the mammals array.
     *   for (let i = 0; i < mammals.length; i += 1) {
     *
     *     // Calculate the y-coordinate.
     *     let y = (i + 1) * 25;
     *
     *     // Get the mammal's common name.
     *     let name = mammals[i].getContent();
     *
     *     // Display the mammal's name.
     *     text(name, 20, y);
     *   }
     *
     *   describe(
     *     'The words "Goat", "Leopard", and "Zebra" written on three separate lines. The text is black on a gray background.'
     *   );
     * }
     *
     * @example
     * let lastMammal;
     *
     * // Load the XML and create a p5.XML object.
     * async function setup() {
     *   await loadXML('assets/animals.xml', handleData);
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(16);
     *
     *   // Display the content of the last mammal element.
     *   text(lastMammal, 50, 50);
     *
     *   describe('The word "Zebra" written in black on a gray background.');
     * }
     *
     * // Get the content of the last mammal element.
     * function handleData(data) {
     *   // Get an array with all mammal elements.
     *   let mammals = data.getChildren('mammal');
     *
     *   // Get the content of the last mammal.
     *   lastMammal = mammals[mammals.length - 1].getContent();
     * }
     *
     * @example
     * let lastMammal;
     *
     * // Load the XML and preprocess it.
     * async function setup() {
     *   await loadXML('assets/animals.xml', handleData, handleError);
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(16);
     *
     *   // Display the content of the last mammal element.
     *   text(lastMammal, 50, 50);
     *
     *   describe('The word "Zebra" written in black on a gray background.');
     * }
     *
     * // Get the content of the last mammal element.
     * function handleData(data) {
     *   // Get an array with all mammal elements.
     *   let mammals = data.getChildren('mammal');
     *
     *   // Get the content of the last mammal.
     *   lastMammal = mammals[mammals.length - 1].getContent();
     * }
     *
     * // Log any errors to the console.
     * function handleError(error) {
     *   console.error('Oops!', error);
     * }
     */
    fn.loadXML = async function (path, successCallback, errorCallback) {
      try{
        const parser = new DOMParser();

        let { data } = await request(path, 'text');
        const parsedDOM = parser.parseFromString(data, 'application/xml');
        data = new p5.XML(parsedDOM);

        if (successCallback) return successCallback(data);
        return data;
      } catch(err) {
        p5._friendlyFileLoadError(1, path);
        if(errorCallback) {
          return errorCallback(err);
        } else {
          throw err;
        }
      }
    };

    /**
     * This method is suitable for fetching files up to size of 64MB.
     *
     * @method loadBytes
     * @param {String|Request}   file            name of the file or URL to load
     * @param {Function} [callback]      function to be executed after <a href="#/p5/loadBytes">loadBytes()</a>
     *                                    completes
     * @param {Function} [errorCallback] function to be executed if there
     *                                    is an error
     * @returns {Promise<Uint8Array>} a Uint8Array containing the loaded buffer
     *
     * @example
     * let data;
     *
     * async function setup() {
     *   createCanvas(100, 100); // Create a canvas
     *   data = await loadBytes('assets/mammals.xml'); // Load the bytes from the XML file
     *
     *   background(255); // Set a white background
     *   fill(0);       // Set text color to black
     *
     *   // Display the first 5 byte values on the canvas in hexadecimal format
     *   for (let i = 0; i < 5; i++) {
     *     let byteHex = data[i].toString(16);
     *     text(byteHex, 10, 18 * (i + 1)); // Adjust spacing as needed
     *   }
     *
     *   describe('no image displayed, displays first 5 bytes of mammals.xml in hexadecimal format');
     * }
     */
    fn.loadBytes = async function (path, successCallback, errorCallback) {
      try{
        let { data } = await request(path, 'arrayBuffer');
        data = new Uint8Array(data);
        if (successCallback) return successCallback(data);
        return data;
      } catch(err) {
        p5._friendlyFileLoadError(6, path);
        if(errorCallback) {
          return errorCallback(err);
        } else {
          throw err;
        }
      }
    };

    /**
     * Loads a file at the given path as a Blob, then returns the resulting data or
     * passes it to a success callback function, if provided. On load, this function
     * returns a `Promise` that resolves to a Blob containing the file data.
     *
     * @method loadBlob
     * @param {String|Request} path - The path or Request object pointing to the file
     *                                you want to load.
     * @param {Function} [successCallback] - Optional. A function to be called if the
     *                                       file successfully loads, receiving the
     *                                       resulting Blob as its only argument.
     * @param {Function} [errorCallback] - Optional. A function to be called if an
     *                                     error occurs during loading; receives the
     *                                     error object as its only argument.
     * @returns {Promise<Blob>} A promise that resolves with the loaded Blob.
     *
     * @example
     * let myBlob;
     *
     * async function setup() {
     *   createCanvas(200, 200);
     *   background(220);
     *   try {
     *     // 1. Load an image file as a Blob.
     *     myBlob = await loadBlob('assets/flower-1.png');
     *
     *     // 2. Convert the Blob into an object URL.
     *     const objectUrl = URL.createObjectURL(myBlob);
     *
     *     // 3. Load that object URL into a p5.Image.
     *     loadImage(objectUrl, (img) => {
     *       // 4. Display the loaded image.
     *       image(img, 0, 0, width, height);
     *     });
     *   } catch (err) {
     *     console.error('Error loading blob:', err);
     *   }
     * }
     */
    fn.loadBlob = async function(path, successCallback, errorCallback) {
      try{
        const { data } = await request(path, 'blob');
        if (successCallback) return successCallback(data);
        return data;
      } catch(err) {
        if(errorCallback) {
          return errorCallback(err);
        } else {
          throw err;
        }
      }
    };

    /**
     * Method for executing an HTTP GET request. If data type is not specified,
     * it will default to `'text'`. This is equivalent to
     * calling <code>httpDo(path, 'GET')</code>. The 'binary' datatype will return
     * a Blob object, and the 'arrayBuffer' datatype will return an ArrayBuffer
     * which can be used to initialize typed arrays (such as Uint8Array).
     *
     * @method httpGet
     * @param  {String|Request}        path       name of the file or url to load
     * @param  {String}        [datatype] "json", "jsonp", "binary", "arrayBuffer",
     *                                    "xml", or "text"
     * @param  {Function}      [callback] function to be executed after
     *                                    <a href="#/p5/httpGet">httpGet()</a> completes, data is passed in
     *                                    as first argument
     * @param  {Function}      [errorCallback] function to be executed if
     *                                    there is an error, response is passed
     *                                    in as first argument
     * @return {Promise} A promise that resolves with the data when the operation
     *                   completes successfully or rejects with the error after
     *                   one occurs.
     * @example
     * // META:norender
     * // Examples use USGS Earthquake API:
     * //   https://earthquake.usgs.gov/fdsnws/event/1/#methods
     * let earthquakes;
     * async function setup() {
     *   // Get the most recent earthquake in the database
     *   let url =
     * 'https://earthquake.usgs.gov/fdsnws/event/1/query?' +
     *     'format=geojson&limit=1&orderby=time';
     *   earthquakes = await httpGet(url, 'json');
     * }
     *
     * function draw() {
     *   if (!earthquakes) {
     *     // Wait until the earthquake data has loaded before drawing.
     *     return;
     *   }
     *   background(200);
     *   // Get the magnitude and name of the earthquake out of the loaded JSON
     *   let earthquakeMag = earthquakes.features[0].properties.mag;
     *   let earthquakeName = earthquakes.features[0].properties.place;
     *   ellipse(width / 2, height / 2, earthquakeMag * 10, earthquakeMag * 10);
     *   textAlign(CENTER);
     *   text(earthquakeName, 0, height - 30, width, 30);
     *   noLoop();
     * }
     */
    /**
     * @method httpGet
     * @param  {String|Request}  path
     * @param  {Function}        callback
     * @param  {Function}        [errorCallback]
     * @return {Promise}
     */
    fn.httpGet = async function (path, datatype='text', successCallback, errorCallback) {
      // p5._validateParameters('httpGet', arguments);

      if (typeof datatype === 'function') {
        errorCallback = successCallback;
        successCallback = datatype;
        datatype = 'text';
      }

      // This is like a more primitive version of the other load functions.
      // If the user wanted to customize more behavior, pass in Request to path.

      return this.httpDo(path, 'GET', datatype, successCallback, errorCallback);
    };

    /**
     * Method for executing an HTTP POST request. If data type is not specified,
     * it will default to `'text'`. This is equivalent to
     * calling <code>httpDo(path, 'POST')</code>.
     *
     * @method httpPost
     * @param  {String|Request} path       name of the file or url to load
     * @param  {Object|Boolean} [data]     param data passed sent with request
     * @param  {String}         [datatype] "json", "jsonp", "xml", or "text".
     *                                    If omitted, <a href="#/p5/httpPost">httpPost()</a> will guess.
     * @param  {Function}       [callback] function to be executed after
     *                                     <a href="#/p5/httpPost">httpPost()</a> completes, data is passed in
     *                                     as first argument
     * @param  {Function}       [errorCallback] function to be executed if
     *                                          there is an error, response is passed
     *                                          in as first argument
     * @return {Promise} A promise that resolves with the data when the operation
     *                   completes successfully or rejects with the error after
     *                   one occurs.
     *
     * @example
     * // Examples use jsonplaceholder.typicode.com for a Mock Data API
     *
     * let url = 'https://jsonplaceholder.typicode.com/posts';
     * let postData = { userId: 1, title: 'p5 Clicked!', body: 'p5.js is very cool.' };
     *
     * function setup() {
     *   createCanvas(100, 100);
     *   background(200);
     * }
     *
     * function mousePressed() {
     *   httpPost(url, postData, 'json', function(result) {
     *     strokeWeight(2);
     *     text(result.body, mouseX, mouseY);
     *   });
     * }
     *
     * @example
     * let url = 'ttps://invalidURL'; // A bad URL that will cause errors
     * let postData = { title: 'p5 Clicked!', body: 'p5.js is very cool.' };
     *
     * function setup() {
     *   createCanvas(100, 100);
     *   background(200);
     * }
     *
     * function mousePressed() {
     *   httpPost(
     *     url,
     *     postData,
     *     'json',
     *     function(result) {
     *       // ... won't be called
     *     },
     *     function(error) {
     *       strokeWeight(2);
     *       text(error.toString(), mouseX, mouseY);
     *     }
     *   );
     * }
     */
    /**
     * @method httpPost
     * @param  {String|Request}    path
     * @param  {Object|Boolean}    data
     * @param  {Function}         [callback]
     * @param  {Function}         [errorCallback]
     * @return {Promise}
     */
    /**
     * @method httpPost
     * @param  {String|Request}    path
     * @param  {Function}         [callback]
     * @param  {Function}         [errorCallback]
     * @return {Promise}
     */
    fn.httpPost = async function (path, data, datatype='text', successCallback, errorCallback) {
      // p5._validateParameters('httpPost', arguments);

      // This behave similarly to httpGet and additional options should be passed
      // as a `Request`` to path. Both method and body will be overridden.
      // Will try to infer correct Content-Type for given data.

      if (typeof data === 'function') {
        // Assume both data and datatype are functions as data should not be function
        successCallback = data;
        errorCallback = datatype;
        data = undefined;
        datatype = 'text';

      } else if (typeof datatype === 'function') {
        // Data is provided but not datatype\
        errorCallback = successCallback;
        successCallback = datatype;
        datatype = 'text';
      }

      let reqData = data;
      let contentType = 'text/plain';
      // Normalize data
      if(data instanceof p5.XML) {
        reqData = data.serialize();
        contentType = 'application/xml';

      } else if(data instanceof p5.Image) {
        reqData = await data.toBlob();
        contentType = 'image/png';

      } else if (typeof data === 'object') {
        reqData = JSON.stringify(data);
        contentType = 'application/json';
      }

      const requestOptions = {
        method: 'POST',
        body: reqData,
        headers: {
          'Content-Type': contentType
        }
      };

      if (reqData) {
        requestOptions.body = reqData;
      }

      const req = new Request(path, requestOptions);

      return this.httpDo(req, 'POST', datatype, successCallback, errorCallback);
    };

    /**
     * Method for executing an HTTP request. If data type is not specified,
     * it will default to `'text'`.
     *
     * This function is meant for more advanced usage of HTTP requests in p5.js. It is
     * best used when a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
     * object is passed to the `path` parameter.
     *
     * This method is suitable for fetching files up to size of 64MB when "GET" is used.
     *
     * @method httpDo
     * @param  {String|Request}   path      name of the file or url to load
     * @param  {String}           [method]    either "GET", "POST", "PUT", "DELETE",
     *                                      or other HTTP request methods
     * @param  {String}          [datatype] "json", "jsonp", "xml", or "text"
     * @param  {Object}          [data]     param data passed sent with request
     * @param  {Function}        [callback] function to be executed after
     *                                      <a href="#/p5/httpGet">httpGet()</a> completes, data is passed in
     *                                      as first argument
     * @param  {Function}        [errorCallback] function to be executed if
     *                                      there is an error, response is passed
     *                                      in as first argument
     * @return {Promise} A promise that resolves with the data when the operation
     *                   completes successfully or rejects with the error after
     *                   one occurs.
     *
     * @example
     * // Examples use USGS Earthquake API:
     * // https://earthquake.usgs.gov/fdsnws/event/1/#methods
     *
     * // displays an animation of all USGS earthquakes
     * let earthquakes;
     * let eqFeatureIndex = 0;
     *
     * function setup() {
     *  createCanvas(100,100);
     *
     *   let url = 'https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson';
     *
     *   const req = new Request(url, {
     *    method: 'GET',
     *    headers: {authorization: 'Bearer secretKey'}
     *  });
     *  // httpDo(path, method, datatype, success, error)
     *
     *   httpDo(
     *     req,
     *     'GET',
     *    'json',
     *     res => {
     *      earthquakes = res;
     *     },
     *    err => {
     *      console.error('Error loading data:', err);
     *    }
     *  );
     * }
     *
     * function draw() {
     *   // wait until the data is loaded
     *   if (!earthquakes || !earthquakes.features[eqFeatureIndex]) {
     *     return;
     *   }
     *   clear();
     *
     *   let feature = earthquakes.features[eqFeatureIndex];
     *   let mag = feature.properties.mag;
     *   let rad = mag / 11 * ((width + height) / 2);
     *   fill(255, 0, 0, 100);
     *   ellipse(width / 2 + random(-2, 2), height / 2 + random(-2, 2), rad, rad);
     *
     *   if (eqFeatureIndex >= earthquakes.features.length) {
     *     eqFeatureIndex = 0;
     *   } else {
     *     eqFeatureIndex += 1;
     *   }
     * }
     */
    /**
     * @method httpDo
     * @param  {String|Request}    path
     * @param  {Function}         [callback]
     * @param  {Function}         [errorCallback]
     * @return {Promise}
     */
    fn.httpDo = async function (
      path,
      method,
      datatype,
      successCallback,
      errorCallback
    ) {
      // This behave similarly to httpGet but even more primitive. The user
      // will most likely want to pass in a Request to path, the only convenience
      // is that datatype will be taken into account to parse the response.

      if(typeof datatype === 'function'){
        errorCallback = successCallback;
        successCallback = datatype;
        datatype = undefined;
      }

      // Try to infer data type if it is defined
      if(!datatype){
        const extension = typeof path === 'string' ?
          path.split('.').pop() :
          path.url.split('.').pop();
        switch(extension) {
          case 'json':
            datatype = 'json';
            break;

          case 'jpg':
          case 'jpeg':
          case 'png':
          case 'webp':
          case 'gif':
            datatype = 'blob';
            break;

          case 'xml':
            // NOTE: still need to normalize type handling/mapping
            // datatype = 'xml';
          case 'txt':
          default:
            datatype = 'text';
        }
      }

      const req = new Request(path, {
        method
      });

      try{
        const { data } = await request(req, datatype);
        if (successCallback) {
          return successCallback(data);
        } else {
          return data;
        }
      } catch(err) {
        if(errorCallback) {
          return errorCallback(err);
        } else {
          throw err;
        }
      }
    };

    /**
     * @module IO
     * @submodule Output
     * @for p5
     */
    // private array of p5.PrintWriter objects
    fn._pWriters = [];

    /**
     * Creates a new <a href="#/p5.PrintWriter">p5.PrintWriter</a> object.
     *
     * <a href="#/p5.PrintWriter">p5.PrintWriter</a> objects provide a way to
     * save a sequence of text data, called the *print stream*, to the user's
     * computer. They're low-level objects that enable precise control of text
     * output. Functions such as
     * <a href="#/p5/saveStrings">saveStrings()</a> and
     * <a href="#/p5/saveJSON">saveJSON()</a> are easier to use for simple file
     * saving.
     *
     * The first parameter, `filename`, is the name of the file to be written. If
     * a string is passed, as in `createWriter('words.txt')`, a new
     * <a href="#/p5.PrintWriter">p5.PrintWriter</a> object will be created that
     * writes to a file named `words.txt`.
     *
     * The second parameter, `extension`, is optional. If a string is passed, as
     * in `createWriter('words', 'csv')`, the first parameter will be interpreted
     * as the file name and the second parameter as the extension.
     *
     * @method createWriter
     * @param {String} name name of the file to create.
     * @param {String} [extension] format to use for the file.
     * @return {p5.PrintWriter} stream for writing data.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(12);
     *
     *   // Display instructions.
     *   text('Double-click to save', 5, 50, 90);
     *
     *   describe('The text "Double-click to save" written in black on a gray background.');
     * }
     *
     * // Save the file when the user double-clicks.
     * function doubleClicked() {
     *   if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) {
     *     // Create a p5.PrintWriter object.
     *     let myWriter = createWriter('xo.txt');
     *
     *     // Add some lines to the print stream.
     *     myWriter.print('XOO');
     *     myWriter.print('OXO');
     *     myWriter.print('OOX');
     *
     *     // Save the file and close the print stream.
     *     myWriter.close();
     *   }
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(12);
     *
     *   // Display instructions.
     *   text('Double-click to save', 5, 50, 90);
     *
     *   describe('The text "Double-click to save" written in black on a gray background.');
     * }
     *
     * // Save the file when the user double-clicks.
     * function doubleClicked() {
     *   if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) {
     *     // Create a p5.PrintWriter object.
     *     // Use the file format .csv.
     *     let myWriter = createWriter('mauna_loa_co2', 'csv');
     *
     *     // Add some lines to the print stream.
     *     myWriter.print('date,ppm_co2');
     *     myWriter.print('1960-01-01,316.43');
     *     myWriter.print('1970-01-01,325.06');
     *     myWriter.print('1980-01-01,337.9');
     *     myWriter.print('1990-01-01,353.86');
     *     myWriter.print('2000-01-01,369.45');
     *     myWriter.print('2020-01-01,413.61');
     *
     *     // Save the file and close the print stream.
     *     myWriter.close();
     *   }
     * }
     */
    fn.createWriter = function (name, extension) {
      let newPW;
      // check that it doesn't already exist
      for (const i in fn._pWriters) {
        if (fn._pWriters[i].name === name) {
          // if a p5.PrintWriter w/ this name already exists...
          // return fn._pWriters[i]; // return it w/ contents intact.
          // or, could return a new, empty one with a unique name:
          newPW = new p5.PrintWriter(name + this.millis(), extension);
          fn._pWriters.push(newPW);
          return newPW;
        }
      }
      newPW = new p5.PrintWriter(name, extension);
      fn._pWriters.push(newPW);
      return newPW;
    };

    /**
     * A class to describe a print stream.
     *
     * Each `p5.PrintWriter` object provides a way to save a sequence of text
     * data, called the *print stream*, to the user's computer. It's a low-level
     * object that enables precise control of text output. Functions such as
     * <a href="#/p5/saveStrings">saveStrings()</a> and
     * <a href="#/p5/saveJSON">saveJSON()</a> are easier to use for simple file
     * saving.
     *
     * Note: <a href="#/p5/createWriter">createWriter()</a> is the recommended way
     * to make an instance of this class.
     *
     * @class p5.PrintWriter
     * @param  {String} filename name of the file to create.
     * @param  {String} [extension] format to use for the file.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(12);
     *
     *   // Display instructions.
     *   text('Double-click to save', 5, 50, 90);
     *
     *   describe('The text "Double-click to save" written in black on a gray background.');
     * }
     *
     * // Save the file when the user double-clicks.
     * function doubleClicked() {
     *   // Create a p5.PrintWriter object.
     *   let myWriter = createWriter('xo.txt');
     *
     *   // Add some lines to the print stream.
     *   myWriter.print('XOO');
     *   myWriter.print('OXO');
     *   myWriter.print('OOX');
     *
     *   // Save the file and close the print stream.
     *   myWriter.close();
     * }
     */
    p5.PrintWriter = function (filename, extension) {
      let self = this;
      this.name = filename;
      this.content = '';

      /**
       * Writes data to the print stream without adding new lines.
       *
       * The parameter, `data`, is the data to write. `data` can be a number or
       * string, as in `myWriter.write('hi')`, or an array of numbers and strings,
       * as in `myWriter.write([1, 2, 3])`. A comma will be inserted between array
       * array elements when they're added to the print stream.
       *
       * @method write
       * @param {String|Number|Array} data data to be written as a string, number,
       *                                   or array of strings and numbers.
       *
       * @example
       * function setup() {
       *   createCanvas(100, 100);
       *
       *   background(200);
       *
       *   // Style the text.
       *   textAlign(LEFT, CENTER);
       *   textFont('Courier New');
       *   textSize(12);
       *
       *   // Display instructions.
       *   text('Double-click to save', 5, 50, 90);
       *
       *   describe('The text "Double-click to save" written in black on a gray background.');
       * }
       *
       * // Save the file when the user double-clicks.
       * function doubleClicked() {
       *   // Create a p5.PrintWriter object.
       *   let myWriter = createWriter('numbers.txt');
       *
       *   // Add some data to the print stream.
       *   myWriter.write('1,2,3,');
       *   myWriter.write(['4', '5', '6']);
       *
       *   // Save the file and close the print stream.
       *   myWriter.close();
       * }
       */
      this.write = function (data) {
        this.content += data;
      };

      /**
       * Writes data to the print stream with new lines added.
       *
       * The parameter, `data`, is the data to write. `data` can be a number or
       * string, as in `myWriter.print('hi')`, or an array of numbers and strings,
       * as in `myWriter.print([1, 2, 3])`. A comma will be inserted between array
       * array elements when they're added to the print stream.
       *
       * @method print
       * @param {String|Number|Array} data data to be written as a string, number,
       *                                   or array of strings and numbers.
       *
       * @example
       * function setup() {
       *   createCanvas(100, 100);
       *
       *   background(200);
       *
       *   // Style the text.
       *   textAlign(LEFT, CENTER);
       *   textFont('Courier New');
       *   textSize(12);
       *
       *   // Display instructions.
       *   text('Double-click to save', 5, 50, 90);
       *
       *   describe('The text "Double-click to save" written in black on a gray background.');
       * }
       *
       * // Save the file when the user double-clicks.
       * function doubleClicked() {
       *   // Create a p5.PrintWriter object.
       *   let myWriter = createWriter('numbers.txt');
       *
       *   // Add some data to the print stream.
       *   myWriter.print('1,2,3,');
       *   myWriter.print(['4', '5', '6']);
       *
       *   // Save the file and close the print stream.
       *   myWriter.close();
       * }
       */
      this.print = function (data) {
        this.content += `${data}\n`;
      };

      /**
       * Clears all data from the print stream.
       *
       * @method clear
       *
       * @example
       * function setup() {
       *   createCanvas(100, 100);
       *
       *   background(200);
       *
       *   // Style the text.
       *   textAlign(LEFT, CENTER);
       *   textFont('Courier New');
       *   textSize(12);
       *
       *   // Display instructions.
       *   text('Double-click to save', 5, 50, 90);
       *
       *   describe('The text "Double-click to save" written in black on a gray background.');
       * }
       *
       * // Save the file when the user double-clicks.
       * function doubleClicked() {
       *   // Create a p5.PrintWriter object.
       *   let myWriter = createWriter('numbers.txt');
       *
       *   // Add some data to the print stream.
       *   myWriter.print('Hello p5*js!');
       *
       *   // Clear the print stream.
       *   myWriter.clear();
       *
       *   // Save the file and close the print stream.
       *   myWriter.close();
       * }
       */
      this.clear = function () {
        this.content = '';
      };

      /**
       * Saves the file and closes the print stream.
       *
       * @method close
       *
       * @example
       * function setup() {
       *   createCanvas(100, 100);
       *
       *   background(200);
       *
       *   // Style the text.
       *   textAlign(LEFT, CENTER);
       *   textFont('Courier New');
       *   textSize(12);
       *
       *   // Display instructions.
       *   text('Double-click to save', 5, 50, 90);
       *
       *   describe('The text "Double-click to save" written in black on a gray background.');
       * }
       *
       * // Save the file when the user double-clicks.
       * function doubleClicked() {
       *   // Create a p5.PrintWriter object.
       *   let myWriter = createWriter('cat.txt');
       *
       *   // Add some data to the print stream.
       *   // ASCII art courtesy Wikipedia:
       *   // https://en.wikipedia.org/wiki/ASCII_art
       *   myWriter.print(' (\\_/) ');
       *   myWriter.print("(='.'=)");
       *   myWriter.print('(")_(")');
       *
       *   // Save the file and close the print stream.
       *   myWriter.close();
       * }
       */
      this.close = function () {
        // convert String to Array for the writeFile Blob
        const arr = [];
        arr.push(this.content);
        fn.writeFile(arr, filename, extension);
        // remove from _pWriters array and delete self
        for (const i in fn._pWriters) {
          if (fn._pWriters[i].name === this.name) {
            // remove from _pWriters array
            fn._pWriters.splice(i, 1);
          }
        }
        self.clear();
        self = {};
      };
    };

    /**
     * @module IO
     * @submodule Output
     * @for p5
     */

    // object, filename, options --> saveJSON, saveStrings,
    // filename, [extension] [canvas] --> saveImage

    /**
     *  Saves a given element(image, text, json, csv, wav, or html) to the client's
     *  computer. The first parameter can be a pointer to element we want to save.
     *  The element can be one of <a href="#/p5.Element">p5.Element</a>,an Array of
     *  Strings, an Array of JSON, a JSON object, a <a href="#/p5.Table">p5.Table
     *  </a>, a <a href="#/p5.Image">p5.Image</a>, or a p5.SoundFile (requires
     *  p5.sound). The second parameter is a filename (including extension).The
     *  third parameter is for options specific to this type of object. This method
     *  will save a file that fits the given parameters.
     *  If it is called without specifying an element, by default it will save the
     *  whole canvas as an image file. You can optionally specify a filename as
     *  the first parameter in such a case.
     *  **Note that it is not recommended to
     *  call this method within draw, as it will open a new save dialog on every
     *  render.**
     *
     * @method save
     * @param  {Object|String} [objectOrFilename]  If filename is provided, will
     *                                             save canvas as an image with
     *                                             either png or jpg extension
     *                                             depending on the filename.
     *                                             If object is provided, will
     *                                             save depending on the object
     *                                             and filename (see examples
     *                                             above).
     * @param  {String} [filename] If an object is provided as the first
     *                               parameter, then the second parameter
     *                               indicates the filename,
     *                               and should include an appropriate
     *                               file extension (see examples above).
     * @param  {Boolean|String} [options]  Additional options depend on
     *                            filetype. For example, when saving JSON,
     *                            <code>true</code> indicates that the
     *                            output will be optimized for filesize,
     *                            rather than readability.
     *
     * @example
     * // META:norender
     * // Saves the canvas as an image
     * cnv = createCanvas(300, 300);
     * save(cnv, 'myCanvas.jpg');
     *
     * // Saves the canvas as an image by default
     * save('myCanvas.jpg');
     * describe('An example for saving a canvas as an image.');
     *
     * @example
     * // META:norender
     * // Saves p5.Image as an image
     * img = createImage(10, 10);
     * save(img, 'myImage.png');
     * describe('An example for saving a p5.Image element as an image.');
     *
     * @example
     * // META:norender
     * // Saves p5.Renderer object as an image
     * obj = createGraphics(100, 100);
     * save(obj, 'myObject.png');
     * describe('An example for saving a p5.Renderer element.');
     *
     * @example
     * // META:norender
     * let myTable = new p5.Table();
     * // Saves table as html file
     * save(myTable, 'myTable.html');
     *
     * // Comma Separated Values
     * save(myTable, 'myTable.csv');
     *
     * // Tab Separated Values
     * save(myTable, 'myTable.tsv');
     *
     * describe(`An example showing how to save a table in formats of
     *   HTML, CSV and TSV.`);
     *
     * @example
     * // META:norender
     * let myJSON = { a: 1, b: true };
     *
     * // Saves pretty JSON
     * save(myJSON, 'my.json');
     *
     * // Optimizes JSON filesize
     * save(myJSON, 'my.json', true);
     *
     * describe('An example for saving JSON to a txt file with some extra arguments.');
     *
     * @example
     * // META:norender
     * // Saves array of strings to text file with line breaks after each item
     * let arrayOfStrings = ['a', 'b'];
     * save(arrayOfStrings, 'my.txt');
     * describe(`An example for saving an array of strings to text file
     *   with line breaks.`);
     */
    fn.save = function (object, _filename, _options) {
      // TODO: parameters is not used correctly
      // parse the arguments and figure out which things we are saving
      const args = arguments;
      // =================================================
      // OPTION 1: saveCanvas...

      // if no arguments are provided, save canvas
      const cnv = this._curElement ? this._curElement.elt : this.elt;
      if (args.length === 0) {
        fn.saveCanvas(cnv);
        return;

      } else if (args[0] instanceof Renderer || args[0] instanceof Graphics) {
        // otherwise, parse the arguments
        // if first param is a p5Graphics, then saveCanvas
        fn.saveCanvas(args[0].canvas, args[1], args[2]);
        return;

      } else if (args.length === 1 && typeof args[0] === 'string') {
        // if 1st param is String and only one arg, assume it is canvas filename
        fn.saveCanvas(cnv, args[0]);

      } else {
        // =================================================
        // OPTION 2: extension clarifies saveStrings vs. saveJSON
        const extension = _checkFileExtension(args[1], args[2])[1];
        switch (extension) {
          case 'json':
            fn.saveJSON(args[0], args[1], args[2]);
            return;
          case 'txt':
            fn.saveStrings(args[0], args[1], args[2]);
            return;
          // =================================================
          // OPTION 3: decide based on object...
          default:
            if (args[0] instanceof Array) {
              fn.saveStrings(args[0], args[1], args[2]);
            } else if (args[0] instanceof p5.Table) {
              fn.saveTable(args[0], args[1], args[2]);
            } else if (args[0] instanceof p5.Image) {
              fn.saveCanvas(args[0].canvas, args[1]);
            } else if (args[0] instanceof p5.SoundFile) {
              fn.saveSound(args[0], args[1], args[2], args[3]);
            }
        }
      }
    };

    /**
     * Saves an `Object` or `Array` to a JSON file.
     *
     * JavaScript Object Notation
     * (<a href="https://developer.mozilla.org/en-US/docs/Glossary/JSON" target="_blank">JSON</a>)
     * is a standard format for sending data between applications. The format is
     * based on JavaScript objects which have keys and values. JSON files store
     * data in an object with strings as keys. Values can be strings, numbers,
     * Booleans, arrays, `null`, or other objects.
     *
     * The first parameter, `json`, is the data to save. The data can be an array,
     * as in `[1, 2, 3]`, or an object, as in
     * `{ x: 50, y: 50, color: 'deeppink' }`.
     *
     * The second parameter, `filename`, is a string that sets the file's name.
     * For example, calling `saveJSON([1, 2, 3], 'data.json')` saves the array
     * `[1, 2, 3]` to a file called `data.json` on the user's computer.
     *
     * The third parameter, `optimize`, is optional. If `true` is passed, as in
     * `saveJSON([1, 2, 3], 'data.json', true)`, then all unneeded whitespace will
     * be removed to reduce the file size.
     *
     * Note: The browser will either save the file immediately or prompt the user
     * with a dialogue window.
     *
     * @method saveJSON
     * @param  {Array|Object} json data to save.
     * @param  {String} filename name of the file to be saved.
     * @param  {Boolean} [optimize] whether to trim unneeded whitespace. Defaults
     *                              to `true`.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(12);
     *
     *   // Display instructions.
     *   text('Double-click to save', 5, 50, 90);
     *
     *   describe('The text "Double-click to save" written in black on a gray background.');
     * }
     *
     * // Save the file when the user double-clicks.
     * function doubleClicked() {
     *   if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) {
     *     // Create an array.
     *     let data = [1, 2, 3];
     *
     *     // Save the JSON file.
     *     saveJSON(data, 'numbers.json');
     *   }
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(12);
     *
     *   // Display instructions.
     *   text('Double-click to save', 5, 50, 90);
     *
     *   describe('The text "Double-click to save" written in black on a gray background.');
     * }
     *
     * // Save the file when the user double-clicks.
     * function doubleClicked() {
     *   if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) {
     *     // Create an object.
     *     let data = { x: mouseX, y: mouseY };
     *
     *     // Save the JSON file.
     *     saveJSON(data, 'state.json');
     *   }
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(12);
     *
     *   // Display instructions.
     *   text('Double-click to save', 5, 50, 90);
     *
     *   describe('The text "Double-click to save" written in black on a gray background.');
     * }
     *
     * // Save the file when the user double-clicks.
     * function doubleClicked() {
     *   if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) {
     *     // Create an object.
     *     let data = { x: mouseX, y: mouseY };
     *
     *     // Save the JSON file and reduce its size.
     *     saveJSON(data, 'state.json', true);
     *   }
     * }
     */
    fn.saveJSON = function (json, filename, optimize) {
      // p5._validateParameters('saveJSON', arguments);
      let stringify;
      if (optimize) {
        stringify = JSON.stringify(json);
      } else {
        stringify = JSON.stringify(json, undefined, 2);
      }
      this.saveStrings(stringify.split('\n'), filename, 'json');
    };

    /**
     * Saves an `Array` of `String`s to a file, one per line.
     *
     * The first parameter, `list`, is an array with the strings to save.
     *
     * The second parameter, `filename`, is a string that sets the file's name.
     * For example, calling `saveStrings(['0', '01', '011'], 'data.txt')` saves
     * the array `['0', '01', '011']` to a file called `data.txt` on the user's
     * computer.
     *
     * The third parameter, `extension`, is optional. If a string is passed, as in
     * `saveStrings(['0', '01', '0`1'], 'data', 'txt')`, the second parameter will
     * be interpreted as the file name and the third parameter as the extension.
     *
     * The fourth parameter, `isCRLF`, is also optional, If `true` is passed, as
     * in `saveStrings(['0', '01', '011'], 'data', 'txt', true)`, then two
     * characters, `\r\n` , will be added to the end of each string to create new
     * lines in the saved file. `\r` is a carriage return (CR) and `\n` is a line
     * feed (LF). By default, only `\n` (line feed) is added to each string in
     * order to create new lines.
     *
     * Note: The browser will either save the file immediately or prompt the user
     * with a dialogue window.
     *
     *  @method saveStrings
     *  @param  {String[]} list data to save.
     *  @param  {String} filename name of file to be saved.
     *  @param  {String} [extension] format to use for the file.
     *  @param  {Boolean} [isCRLF] whether to add `\r\n` to the end of each
     *                             string. Defaults to `false`.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(12);
     *
     *   // Display instructions.
     *   text('Double-click to save', 5, 50, 90);
     *
     *   describe('The text "Double-click to save" written in black on a gray background.');
     * }
     *
     * // Save the file when the user double-clicks.
     * function doubleClicked() {
     *   if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) {
     *     // Create an array.
     *     let data = ['0', '01', '011'];
     *
     *     // Save the text file.
     *     saveStrings(data, 'data.txt');
     *   }
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(12);
     *
     *   // Display instructions.
     *   text('Double-click to save', 5, 50, 90);
     *
     *   describe('The text "Double-click to save" written in black on a gray background.');
     * }
     *
     * // Save the file when the user double-clicks.
     * function doubleClicked() {
     *   if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) {
     *     // Create an array.
     *     // ASCII art courtesy Wikipedia:
     *     // https://en.wikipedia.org/wiki/ASCII_art
     *     let data = [' (\\_/) ', "(='.'=)", '(")_(")'];
     *
     *     // Save the text file.
     *     saveStrings(data, 'cat', 'txt');
     *   }
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(12);
     *
     *   // Display instructions.
     *   text('Double-click to save', 5, 50, 90);
     *
     *   describe('The text "Double-click to save" written in black on a gray background.');
     * }
     *
     * // Save the file when the user double-clicks.
     * function doubleClicked() {
     *   if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) {
     *     // Create an array.
     *     //   +--+
     *     //  /  /|
     *     // +--+ +
     *     // |  |/
     *     // +--+
     *     let data = ['  +--+', ' /  /|', '+--+ +', '|  |/', '+--+'];
     *
     *     // Save the text file.
     *     // Use CRLF for line endings.
     *     saveStrings(data, 'box', 'txt', true);
     *   }
     * }
     */
    fn.saveStrings = function (list, filename, extension, isCRLF) {
      // p5._validateParameters('saveStrings', arguments);
      const ext = extension || 'txt';
      const pWriter = new p5.PrintWriter(filename, ext);
      for (let item of list) {
        isCRLF ? pWriter.write(item + '\r\n') : pWriter.write(item + '\n');
      }
      pWriter.close();
      pWriter.clear();
    };

    // =======
    // HELPERS
    // =======

    function escapeHelper(content) {
      return content
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;');
    }

    /**
     * Writes the contents of a <a href="#/p5.Table">Table</a> object to a file. Defaults to a
     * text file with comma-separated-values ('csv') but can also
     * use tab separation ('tsv'), or generate an HTML table ('html').
     * The file saving process and location of the saved file will
     * vary between web browsers.
     *
     * @method saveTable
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param  {p5.Table} Table  the <a href="#/p5.Table">Table</a> object to save to a file
     * @param  {String} filename the filename to which the Table should be saved
     * @param  {String} [options]  can be one of "tsv", "csv", or "html"
     * @example
     * let table;
     *
     * function setup() {
     *   table = new p5.Table();
     *
     *   table.addColumn('id');
     *   table.addColumn('species');
     *   table.addColumn('name');
     *
     *   let newRow = table.addRow();
     *   newRow.setNum('id', table.getRowCount() - 1);
     *   newRow.setString('species', 'Panthera leo');
     *   newRow.setString('name', 'Lion');
     *
     *   // To save, un-comment next line then click 'run'
     *   // saveTable(table, 'new.csv');
     *
     *   describe('no image displayed');
     * }
     *
     * // Saves the following to a file called 'new.csv':
     * // id,species,name
     * // 0,Panthera leo,Lion
     */
    fn.saveTable = function (table, filename, options) {
      // p5._validateParameters('saveTable', arguments);
      let ext;
      if (options === undefined) {
        ext = filename.substring(filename.lastIndexOf('.') + 1, filename.length);
        if(ext === filename) ext = 'csv';
      } else {
        ext = options;
      }
      const pWriter = this.createWriter(filename, ext);

      const header = table.columns;

      let sep = ','; // default to CSV
      if (ext === 'tsv') {
        sep = '\t';
      }
      if (ext !== 'html') {
        const output = table.toString(sep);
        pWriter.write(output);
      } else {
        // otherwise, make HTML
        pWriter.print('<html>');
        pWriter.print('<head>');
        let str = '  <meta http-equiv="content-type" content';
        str += '="text/html;charset=utf-8" />';
        pWriter.print(str);
        pWriter.print('</head>');

        pWriter.print('<body>');
        pWriter.print('  <table>');

        // make header if it has values
        if (header[0] !== '0') {
          pWriter.print('    <tr>');
          for (let k = 0; k < header.length; k++) {
            const e = escapeHelper(header[k]);
            pWriter.print(`      <td>${e}`);
            pWriter.print('      </td>');
          }
          pWriter.print('    </tr>');
        }

        // make rows
        for (let row = 0; row < table.rows.length; row++) {
          pWriter.print('    <tr>');
          for (let col = 0; col < table.columns.length; col++) {
            const entry = table.rows[row].getString(col);
            const htmlEntry = escapeHelper(entry);
            pWriter.print(`      <td>${htmlEntry}`);
            pWriter.print('      </td>');
          }
          pWriter.print('    </tr>');
        }
        pWriter.print('  </table>');
        pWriter.print('</body>');
        pWriter.print('</html>');
      }
      // close and clear the pWriter
      pWriter.close();
      pWriter.clear();
    }; // end saveTable()

    /**
     *  Generate a blob of file data as a url to prepare for download.
     *  Accepts an array of data, a filename, and an extension (optional).
     *  This is a private function because it does not do any formatting,
     *  but it is used by <a href="#/p5/saveStrings">saveStrings</a>, <a href="#/p5/saveJSON">saveJSON</a>, <a href="#/p5/saveTable">saveTable</a> etc.
     *
     *  @param  {Array} dataToDownload
     *  @param  {String} filename
     *  @param  {String} [extension]
     *  @private
     */
    fn.writeFile = function (dataToDownload, filename, extension) {
      let type = 'application/octet-stream';
      if (fn._isSafari()) {
        type = 'text/plain';
      }
      const blob = new Blob(dataToDownload, {
        type
      });
      fn.downloadFile(blob, filename, extension);
    };

    /**
     *  Forces download. Accepts a url to filedata/blob, a filename,
     *  and an extension (optional).
     *  This is a private function because it does not do any formatting,
     *  but it is used by <a href="#/p5/saveStrings">saveStrings</a>, <a href="#/p5/saveJSON">saveJSON</a>, <a href="#/p5/saveTable">saveTable</a> etc.
     *
     *  @method downloadFile
     *  @private
     *  @param  {String|Blob} data    either an href generated by createObjectURL,
     *                                or a Blob object containing the data
     *  @param  {String} [filename]
     *  @param  {String} [extension]
     */
    fn.downloadFile = downloadFile;

    /**
     *  Returns a file extension, or another string
     *  if the provided parameter has no extension.
     *
     *  @param   {String} filename
     *  @param   {String} [extension]
     *  @return  {String[]} [fileName, fileExtension]
     *
     *  @private
     */
    fn._checkFileExtension = _checkFileExtension;

    /**
     *  Returns true if the browser is Safari, false if not.
     *  Safari makes trouble for downloading files.
     *
     *  @return  {Boolean} [description]
     *  @private
     */
    fn._isSafari = function () {
      // The following line is CC BY SA 3 by user Fregante https://stackoverflow.com/a/23522755
      return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
    };
  }

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

  var X={trailer:59};function F(t=256){let e=0,s=new Uint8Array(t);return {get buffer(){return s.buffer},reset(){e=0;},bytesView(){return s.subarray(0,e)},bytes(){return s.slice(0,e)},writeByte(r){n(e+1),s[e]=r,e++;},writeBytes(r,o=0,i=r.length){n(e+i);for(let c=0;c<i;c++)s[e++]=r[c+o];},writeBytesView(r,o=0,i=r.byteLength){n(e+i),s.set(r.subarray(o,o+i),e),e+=i;}};function n(r){var o=s.length;if(o>=r)return;var i=1024*1024;r=Math.max(r,o*(o<i?2:1.125)>>>0),o!=0&&(r=Math.max(r,256));let c=s;s=new Uint8Array(r),e>0&&s.set(c.subarray(0,e),0);}}var O=12,J=5003,lt=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535];function at(t,e,s,n,r=F(512),o=new Uint8Array(256),i=new Int32Array(J),c=new Int32Array(J)){let x=i.length,a=Math.max(2,n);o.fill(0),c.fill(0),i.fill(-1);let l=0,f=0,g=a+1,h=g,b=false,w=h,_=(1<<w)-1,u=1<<g-1,k=u+1,B=u+2,p=0,A=s[0],z=0;for(let y=x;y<65536;y*=2)++z;z=8-z,r.writeByte(a),I(u);let d=s.length;for(let y=1;y<d;y++){t:{let m=s[y],v=(m<<O)+A,M=m<<z^A;if(i[M]===v){A=c[M];break t}let V=M===0?1:x-M;for(;i[M]>=0;)if(M-=V,M<0&&(M+=x),i[M]===v){A=c[M];break t}I(A),A=m,B<1<<O?(c[M]=B++,i[M]=v):(i.fill(-1),B=u+2,b=true,I(u));}}return I(A),I(k),r.writeByte(0),r.bytesView();function I(y){for(l&=lt[f],f>0?l|=y<<f:l=y,f+=w;f>=8;)o[p++]=l&255,p>=254&&(r.writeByte(p),r.writeBytesView(o,0,p),p=0),l>>=8,f-=8;if((B>_||b)&&(b?(w=h,_=(1<<w)-1,b=false):(++w,_=w===O?1<<w:(1<<w)-1)),y==k){for(;f>0;)o[p++]=l&255,p>=254&&(r.writeByte(p),r.writeBytesView(o,0,p),p=0),l>>=8,f-=8;p>0&&(r.writeByte(p),r.writeBytesView(o,0,p),p=0);}}}var $=at;function D(t,e,s){return t<<8&63488|e<<2&992|s>>3}function G(t,e,s,n){return t>>4|e&240|(s&240)<<4|(n&240)<<8}function j(t,e,s){return t>>4<<8|e&240|s>>4}function R(t,e,s){return t<e?e:t>s?s:t}function T(t){return t*t}function tt(t,e,s){var n=0,r=1e100;let o=t[e],i=o.cnt,x=o.rc,a=o.gc,l=o.bc;for(var f=o.fw;f!=0;f=t[f].fw){let h=t[f],b=h.cnt,w=i*b/(i+b);if(!(w>=r)){var g=0;(g+=w*T(h.rc-x),!(g>=r)&&(g+=w*T(h.gc-a),!(g>=r)&&(g+=w*T(h.bc-l),!(g>=r)&&(r=g,n=f))));}}o.err=r,o.nn=n;}function Q(){return {ac:0,rc:0,gc:0,bc:0,cnt:0,nn:0,fw:0,bk:0,tm:0,mtm:0,err:0}}function ut(t,e){let s=e==="rgb444"?4096:65536,n=new Array(s),r=t.length;if(e==="rgba4444")for(let o=0;o<r;++o){let i=t[o],c=i>>24&255,x=i>>16&255,a=i>>8&255,l=i&255,f=G(l,a,x,c),g=f in n?n[f]:n[f]=Q();g.rc+=l,g.gc+=a,g.bc+=x,g.ac+=c,g.cnt++;}else if(e==="rgb444")for(let o=0;o<r;++o){let i=t[o],c=i>>16&255,x=i>>8&255,a=i&255,l=j(a,x,c),f=l in n?n[l]:n[l]=Q();f.rc+=a,f.gc+=x,f.bc+=c,f.cnt++;}else for(let o=0;o<r;++o){let i=t[o],c=i>>16&255,x=i>>8&255,a=i&255,l=D(a,x,c),f=l in n?n[l]:n[l]=Q();f.rc+=a,f.gc+=x,f.bc+=c,f.cnt++;}return n}function H(t,e,s={}){let{format:n="rgb565",clearAlpha:r=true,clearAlphaColor:o=0,clearAlphaThreshold:i=0,oneBitAlpha:c=false}=s;if(!t||!t.buffer)throw new Error("quantize() expected RGBA Uint8Array data");if(!(t instanceof Uint8Array)&&!(t instanceof Uint8ClampedArray))throw new Error("quantize() expected RGBA Uint8Array data");let x=new Uint32Array(t.buffer),a=s.useSqrt!==false,l=n==="rgba4444",f=ut(x,n),g=f.length,h=g-1,b=new Uint32Array(g+1);for(var w=0,u=0;u<g;++u){let C=f[u];if(C!=null){var _=1/C.cnt;l&&(C.ac*=_),C.rc*=_,C.gc*=_,C.bc*=_,f[w++]=C;}}T(e)/w<.022&&(a=false);for(var u=0;u<w-1;++u)f[u].fw=u+1,f[u+1].bk=u,a&&(f[u].cnt=Math.sqrt(f[u].cnt));a&&(f[u].cnt=Math.sqrt(f[u].cnt));var k,B,p;for(u=0;u<w;++u){tt(f,u);var A=f[u].err;for(B=++b[0];B>1&&(p=B>>1,!(f[k=b[p]].err<=A));B=p)b[B]=k;b[B]=u;}var z=w-e;for(u=0;u<z;){for(var d;;){var I=b[1];if(d=f[I],d.tm>=d.mtm&&f[d.nn].mtm<=d.tm)break;d.mtm==h?I=b[1]=b[b[0]--]:(tt(f,I),d.tm=u);var A=f[I].err;for(B=1;(p=B+B)<=b[0]&&(p<b[0]&&f[b[p]].err>f[b[p+1]].err&&p++,!(A<=f[k=b[p]].err));B=p)b[B]=k;b[B]=I;}var y=f[d.nn],m=d.cnt,v=y.cnt,_=1/(m+v);l&&(d.ac=_*(m*d.ac+v*y.ac)),d.rc=_*(m*d.rc+v*y.rc),d.gc=_*(m*d.gc+v*y.gc),d.bc=_*(m*d.bc+v*y.bc),d.cnt+=y.cnt,d.mtm=++u,f[y.bk].fw=y.fw,f[y.fw].bk=y.bk,y.mtm=h;}let M=[];var V=0;for(u=0;;++V){let L=R(Math.round(f[u].rc),0,255),C=R(Math.round(f[u].gc),0,255),Y=R(Math.round(f[u].bc),0,255),E=255;if(l){if(E=R(Math.round(f[u].ac),0,255),c){let st=typeof c=="number"?c:127;E=E<=st?0:255;}r&&E<=i&&(L=C=Y=o,E=0);}let K=l?[L,C,Y,E]:[L,C,Y];if(xt(M,K)||M.push(K),(u=f[u].fw)==0)break}return M}function xt(t,e){for(let s=0;s<t.length;s++){let n=t[s],r=n[0]===e[0]&&n[1]===e[1]&&n[2]===e[2],o=n.length>=4&&e.length>=4?n[3]===e[3]:true;if(r&&o)return  true}return  false}function U(t,e){var s=0,n;for(n=0;n<t.length;n++){let r=t[n]-e[n];s+=r*r;}return s}function W(t,e,s=U){let n=Infinity,r=-1;for(let o=0;o<t.length;o++){let i=t[o],c=s(e,i);c<n&&(n=c,r=o);}return r}function ct(t={}){let{initialCapacity:e=4096,auto:s=true}=t,n=F(e),r=5003,o=new Uint8Array(256),i=new Int32Array(r),c=new Int32Array(r),x=false;return {reset(){n.reset(),x=false;},finish(){n.writeByte(X.trailer);},bytes(){return n.bytes()},bytesView(){return n.bytesView()},get buffer(){return n.buffer},get stream(){return n},writeHeader:a,writeFrame(l,f,g,h={}){let{transparent:b=false,transparentIndex:w=0,delay:_=0,palette:u=null,repeat:k=0,colorDepth:B=8,dispose:p=-1}=h,A=false;if(s?x||(A=true,a(),x=true):A=Boolean(h.first),f=Math.max(0,Math.floor(f)),g=Math.max(0,Math.floor(g)),A){if(!u)throw new Error("First frame must include a { palette } option");pt(n,f,g,u,B),it(n,u),k>=0&&dt(n,k);}let z=Math.round(_/10);wt(n,p,z,b,w);let d=Boolean(u)&&!A;ht(n,f,g,d?u:null),d&&it(n,u),yt(n,l,f,g,B,o,i,c);}};function a(){ft(n,"GIF89a");}}function wt(t,e,s,n,r){t.writeByte(33),t.writeByte(249),t.writeByte(4),r<0&&(r=0,n=false);var o,i;n?(o=1,i=2):(o=0,i=0),e>=0&&(i=e&7),i<<=2;let c=0;t.writeByte(0|i|c|o),S(t,s),t.writeByte(r||0),t.writeByte(0);}function pt(t,e,s,n,r=8){let o=1,i=0,c=Z(n.length)-1,x=o<<7|r-1<<4|i<<3|c,a=0,l=0;S(t,e),S(t,s),t.writeBytes([x,a,l]);}function dt(t,e){t.writeByte(33),t.writeByte(255),t.writeByte(11),ft(t,"NETSCAPE2.0"),t.writeByte(3),t.writeByte(1),S(t,e),t.writeByte(0);}function it(t,e){let s=1<<Z(e.length);for(let n=0;n<s;n++){let r=[0,0,0];n<e.length&&(r=e[n]),t.writeByte(r[0]),t.writeByte(r[1]),t.writeByte(r[2]);}}function ht(t,e,s,n){if(t.writeByte(44),S(t,0),S(t,0),S(t,e),S(t,s),n){let r=0,o=0,i=Z(n.length)-1;t.writeByte(128|r|o|0|i);}else t.writeByte(0);}function yt(t,e,s,n,r=8,o,i,c){$(s,n,e,r,t,o,i,c);}function S(t,e){t.writeByte(e&255),t.writeByte(e>>8&255);}function ft(t,e){for(var s=0;s<e.length;s++)t.writeByte(e.charCodeAt(s));}function Z(t){return Math.max(Math.ceil(Math.log2(t)),1)}

  /**
   * @module Image
   * @submodule Loading & Displaying
   * @for p5
   * @requires core
   */


  function loadingDisplaying(p5, fn){
    /**
     * Loads an image to create a <a href="#/p5.Image">p5.Image</a> object.
     *
     * `loadImage()` interprets the first parameter one of three ways. If the path
     * to an image file is provided, `loadImage()` will load it. Paths to local
     * files should be relative, such as `'assets/thundercat.jpg'`. URLs such as
     * `'https://example.com/thundercat.jpg'` may be blocked due to browser
     * security. Raw image data can also be passed as a base64 encoded image in
     * the form `'data:image/png;base64,arandomsequenceofcharacters'`. The `path`
     * parameter can also be defined as a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
     * object for more advanced usage.
     *
     * The second parameter is optional. If a function is passed, it will be
     * called once the image has loaded. The callback function can optionally use
     * the new <a href="#/p5.Image">p5.Image</a> object. The return value of the
     * function will be used as the final return value of `loadImage()`.
     *
     * The third parameter is also optional. If a function is passed, it will be
     * called if the image fails to load. The callback function can optionally use
     * the event error. The return value of the function will be used as the final
     * return value of `loadImage()`.
     *
     * This function returns a `Promise` and should be used in an `async` setup with
     * `await`. See the examples for the usage syntax.
     *
     * @method loadImage
     * @param  {String|Request}      path  path of the image to be loaded or base64 encoded image.
     * @param  {function(p5.Image)} [successCallback] function called with
     *                               <a href="#/p5.Image">p5.Image</a> once it
     *                               loads.
     * @param  {function(Event)}    [failureCallback] function called with event
     *                               error if the image fails to load.
     * @return {Promise<p5.Image>}   the <a href="#/p5.Image">p5.Image</a> object.
     *
     * @example
     * let img;
     *
     * // Load the image and create a p5.Image object.
     * async function setup() {
     *   img = await loadImage('assets/laDefense.jpg');
     *   createCanvas(100, 100);
     *
     *   // Draw the image.
     *   image(img, 0, 0);
     *
     *   describe('Image of the underside of a white umbrella and a gridded ceiling.');
     * }
     *
     * @example
     * async function setup() {
     *   // Call handleImage() once the image loads.
     *   await loadImage('assets/laDefense.jpg', handleImage);
     *
     *   describe('Image of the underside of a white umbrella and a gridded ceiling.');
     * }
     *
     * // Display the image.
     * function handleImage(img) {
     *   image(img, 0, 0);
     * }
     *
     * @example
     * async function setup() {
     *   // Call handleImage() once the image loads or
     *   // call handleError() if an error occurs.
     *   await loadImage('assets/laDefense.jpg', handleImage, handleError);
     * }
     *
     * // Display the image.
     * function handleImage(img) {
     *   image(img, 0, 0);
     *
     *   describe('Image of the underside of a white umbrella and a gridded ceiling.');
     * }
     *
     * // Log the error.
     * function handleError(event) {
     *   console.error('Oops!', event);
     * }
     */
    fn.loadImage = async function(
      path,
      successCallback,
      failureCallback
    ) {
      // p5._validateParameters('loadImage', arguments);

      try{
        let pImg = new p5.Image(1, 1, this);

        const req = new Request(path, {
          method: 'GET',
          mode: 'cors'
        });

        const { data, headers } = await request(req, 'bytes');

        // GIF section
        const contentType = headers.get('content-type');

        if (contentType === null) {
          console.warn(
            'The image you loaded does not have a Content-Type header. If you are using the online editor consider reuploading the asset.'
          );
        }

        if (contentType && contentType.includes('image/gif')) {
          await _createGif(
            data,
            pImg
          );

        } else {
          // Non-GIF Section
          const blob = new Blob([data]);
          const img = await createImageBitmap(blob);

          pImg.width = pImg.canvas.width = img.width;
          pImg.height = pImg.canvas.height = img.height;

          // Draw the image into the backing canvas of the p5.Image
          pImg.drawingContext.drawImage(img, 0, 0);
        }

        pImg.modified = true;

        if(successCallback){
          return successCallback(pImg);
        }else {
          return pImg;
        }

      } catch(err) {
        p5._friendlyFileLoadError(0, path);
        if (typeof failureCallback === 'function') {
          return failureCallback(err);
        } else {
          throw err;
        }
      }
    };

    /**
     * Generates a gif from a sketch and saves it to a file.
     *
     * `saveGif()` may be called in <a href="#/p5/setup">setup()</a> or at any
     * point while a sketch is running.
     *
     * The first parameter, `fileName`, sets the gif's file name.
     *
     * The second parameter, `duration`, sets the gif's duration in seconds.
     *
     * The third parameter, `options`, is optional. If an object is passed,
     * `saveGif()` will use its properties to customize the gif. `saveGif()`
     * recognizes the properties `delay`, `units`, `silent`,
     * `notificationDuration`, and `notificationID`.
     *
     * @method saveGif
     * @param  {String} filename file name of gif.
     * @param  {Number} duration duration in seconds to capture from the sketch.
     * @param  {Object} [options] an object that can contain five more properties:
     * @param {Number} [options.delay=0] How much time to wait before recording.
     * @param {'seconds'|'frames'} [options.units='seconds'] The units of the duration and delay.
     * @param {Boolean} [options.silent=false] Whether to show progress notifications.
     * @param {Number} [options.notificationDuration=0] How long in seconds the final notification will live, or 0 for it to remain permanently.
     * @param {String} [options.notificationID='progressBar'] The id to give to the notification's DOM element.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A circle drawn in the middle of a gray square. The circle changes color from black to white, then repeats.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the circle.
     *   let c = frameCount % 255;
     *   fill(c);
     *
     *   // Display the circle.
     *   circle(50, 50, 25);
     * }
     *
     * // Save a 5-second gif when the user presses the 's' key.
     * function keyPressed() {
     *   if (key === 's') {
     *     saveGif('mySketch', 5);
     *   }
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A circle drawn in the middle of a gray square. The circle changes color from black to white, then repeats.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the circle.
     *   let c = frameCount % 255;
     *   fill(c);
     *
     *   // Display the circle.
     *   circle(50, 50, 25);
     * }
     *
     * // Save a 5-second gif when the user presses the 's' key.
     * // Wait 1 second after the key press before recording.
     * function keyPressed() {
     *   if (key === 's') {
     *     saveGif('mySketch', 5, { delay: 1 });
     *   }
     * }
     */
    fn.saveGif = async function(
      fileName,
      duration,
      options = {
        delay: 0,
        units: 'seconds',
        silent: false,
        notificationDuration: 0,
        notificationID: 'progressBar',
        reset: true
      }
    ) {
      // validate parameters
      if (typeof fileName !== 'string') {
        throw TypeError('fileName parameter must be a string');
      }
      if (typeof duration !== 'number') {
        throw TypeError('Duration parameter must be a number');
      }

      // extract variables for more comfortable use
      const delay = (options && options.delay) || 0;  // in seconds
      const units = (options && options.units) || 'seconds';  // either 'seconds' or 'frames'
      const silent = (options && options.silent) || false;
      const notificationDuration = (options && options.notificationDuration) || 0;
      const notificationID = (options && options.notificationID) || 'progressBar';
      const resetAnimation = (options && options.reset !== undefined) ? options.reset : true;
      // if arguments in the options object are not correct, cancel operation
      if (typeof delay !== 'number') {
        throw TypeError('Delay parameter must be a number');
      }
      // if units is not seconds nor frames, throw error
      if (units !== 'seconds' && units !== 'frames') {
        throw TypeError('Units parameter must be either "frames" or "seconds"');
      }

      if (typeof silent !== 'boolean') {
        throw TypeError('Silent parameter must be a boolean');
      }

      if (typeof notificationDuration !== 'number') {
        throw TypeError('Notification duration parameter must be a number');
      }

      if (typeof notificationID !== 'string') {
        throw TypeError('Notification ID parameter must be a string');
      }

      this._recording = true;

      // get the project's framerate
      let _frameRate = this._targetFrameRate;
      // if it is undefined or some non useful value, assume it's 60
      if (
        _frameRate === Infinity ||
        _frameRate === undefined ||
        _frameRate === 0
      ) {
        _frameRate = 60;
      }

      // calculate frame delay based on frameRate

      // this delay has nothing to do with the
      // delay in options, but rather is the delay
      // we have to specify to the gif encoder between frames.
      let gifFrameDelay = 1 / _frameRate * 1000;

      // constrain it to be always greater than 20,
      // otherwise it won't work in some browsers and systems
      // reference: https://stackoverflow.com/questions/64473278/gif-frame-duration-seems-slower-than-expected
      gifFrameDelay = gifFrameDelay < 20 ? 20 : gifFrameDelay;

      // check the mode we are in and how many frames
      // that duration translates to
      const nFrames = units === 'seconds' ? duration * _frameRate : duration;
      const nFramesDelay = units === 'seconds' ? delay * _frameRate : delay;

      // initialize variables for the frames processing
      let frameIterator;
      let totalNumberOfFrames;

      if (resetAnimation) {
        frameIterator = nFramesDelay;
        this.frameCount = frameIterator;
        totalNumberOfFrames = nFrames + nFramesDelay;
      } else {
        frameIterator = this.frameCount + nFramesDelay;
        totalNumberOfFrames = frameIterator + nFrames;
      }

      const lastPixelDensity = this._renderer._pixelDensity;
      this.pixelDensity(1);

      // We first take every frame that we are going to use for the animation
      let frames = [];

      if (document.getElementById(notificationID) !== null)
        document.getElementById(notificationID).remove();

      let p;
      if (!silent){
        p = this.createP('');
        p.id(notificationID);
        p.style('font-size', '16px');
        p.style('font-family', 'Montserrat');
        p.style('background-color', '#ffffffa0');
        p.style('padding', '8px');
        p.style('border-radius', '10px');
        p.position(0, 0);
      }

      let pixels;
      let gl;
      if (this._renderer instanceof p5.RendererGL) {
        // if we have a WEBGL context, initialize the pixels array
        // and the gl context to use them inside the loop
        gl = this.drawingContext;
        pixels = new Uint8Array(
          gl.drawingBufferWidth * gl.drawingBufferHeight * 4
        );
      }

      // stop the loop since we are going to manually redraw
      this.noLoop();

      // Defer execution until the rest of the call stack finishes, allowing the
      // rest of `setup` to be called (and, importantly, canvases hidden in setup
      // to be unhidden.)
      //
      // Waiting on this empty promise means we'll continue as soon as setup
      // finishes without waiting for another frame.
      await new Promise(requestAnimationFrame);

      while (frameIterator < totalNumberOfFrames) {
        /*
          we draw the next frame. this is important, since
          busy sketches or low end devices might take longer
          to render some frames. So we just wait for the frame
          to be drawn and immediately save it to a buffer and continue
        */
        this.redraw();
        await new Promise(requestAnimationFrame);
        // depending on the context we'll extract the pixels one way
        // or another
        let data = undefined;

        if (this._renderer instanceof p5.RendererGL) {
          pixels = new Uint8Array(
            gl.drawingBufferWidth * gl.drawingBufferHeight * 4
          );
          gl.readPixels(
            0,
            0,
            gl.drawingBufferWidth,
            gl.drawingBufferHeight,
            gl.RGBA,
            gl.UNSIGNED_BYTE,
            pixels
          );

          data = _flipPixels(pixels, this.width, this.height);
        } else {
          data = this.drawingContext.getImageData(0, 0, this.width, this.height)
            .data;
        }

        frames.push(data);
        frameIterator++;

        if (!silent) {
          p.html(
            'Saved frame <b>' +
            frames.length.toString() +
            '</b> out of ' +
            nFrames.toString()
          );
        }
        await new Promise(resolve => setTimeout(resolve, 0));
      }
      if (!silent) p.html('Frames processed, generating color palette...');

      this.loop();
      this.pixelDensity(lastPixelDensity);

      // create the gif encoder and the colorspace format
      const gif = ct();

      // calculate the global palette for this set of frames
      const globalPalette = _generateGlobalPalette(frames);

      // Rather than using applyPalette() from the gifenc library, we use our
      // own function to map frame pixels to a palette color. This way, we can
      // cache palette color mappings between frames for extra performance, and
      // use our own caching mechanism to avoid flickering colors from cache
      // key collisions.
      const paletteCache = {};
      const getIndexedFrame = frame => {
        const length = frame.length / 4;
        const index = new Uint8Array(length);
        for (let i = 0; i < length; i++) {
          const key =
            (frame[i * 4] << 24) |
            (frame[i * 4 + 1] << 16) |
            (frame[i * 4 + 2] << 8) |
            frame[i * 4 + 3];
          if (paletteCache[key] === undefined) {
            paletteCache[key] = W(
              globalPalette,
              frame.slice(i * 4, (i + 1) * 4)
            );
          }
          index[i] = paletteCache[key];
        }
        return index;
      };

      // the way we designed the palette means we always take the last index for transparency
      const transparentIndex = globalPalette.length - 1;

      // we are going to iterate the frames in pairs, n-1 and n
      let prevIndexedFrame = [];
      for (let i = 0; i < frames.length; i++) {
        //const indexedFrame = applyPalette(frames[i], globalPaletteWithoutAlpha, 'rgba565');
        const indexedFrame = getIndexedFrame(frames[i]);

        // Make a copy of the palette-applied frame before editing the original
        // to use transparent pixels
        const originalIndexedFrame = indexedFrame.slice();

        if (i === 0) {
          gif.writeFrame(indexedFrame, this.width, this.height, {
            palette: globalPalette,
            delay: gifFrameDelay,
            dispose: 1
          });
        } else {
          // Matching pixels between frames can be set to full transparency,
          // allowing the previous frame's pixels to show through. We only do
          // this for pixels that get mapped to the same quantized color so that
          // the resulting image would be the same.
          for (let i = 0; i < indexedFrame.length; i++) {
            if (indexedFrame[i] === prevIndexedFrame[i]) {
              indexedFrame[i] = transparentIndex;
            }
          }

          // Write frame into the encoder
          gif.writeFrame(indexedFrame, this.width, this.height, {
            delay: gifFrameDelay,
            transparent: true,
            transparentIndex,
            dispose: 1
          });
        }

        prevIndexedFrame = originalIndexedFrame;

        if (!silent) {
          p.html(
            'Rendered frame <b>' + i.toString() + '</b> out of ' + nFrames.toString()
          );
        }


        // this just makes the process asynchronous, preventing
        // that the encoding locks up the browser
        await new Promise(resolve => setTimeout(resolve, 0));
      }

      gif.finish();

      // Get a direct typed array view into the buffer to avoid copying it
      const buffer = gif.bytesView();
      const extension = 'gif';

      const blob = new Blob([buffer], {
        type: 'image/gif'
      });

      frames = [];
      this._recording = false;
      this.loop();

      if (!silent){
        p.html('Done. Downloading your gif!🌸');
        if(notificationDuration > 0)
          setTimeout(() => p.remove(), notificationDuration * 1000);
      }

      fn.downloadFile(blob, fileName, extension);
    };

    function _flipPixels(pixels, width, height) {
      // extracting the pixels using readPixels returns
      // an upside down image. we have to flip it back
      // first. this solution is proposed by gman on
      // this stack overflow answer:
      // https://stackoverflow.com/questions/41969562/how-can-i-flip-the-result-of-webglrenderingcontext-readpixels

      const halfHeight = parseInt(height / 2);
      const bytesPerRow = width * 4;

      // make a temp buffer to hold one row
      const temp = new Uint8Array(width * 4);
      for (let y = 0; y < halfHeight; ++y) {
        const topOffset = y * bytesPerRow;
        const bottomOffset = (height - y - 1) * bytesPerRow;

        // make copy of a row on the top half
        temp.set(pixels.subarray(topOffset, topOffset + bytesPerRow));

        // copy a row from the bottom half to the top
        pixels.copyWithin(topOffset, bottomOffset, bottomOffset + bytesPerRow);

        // copy the copy of the top half row to the bottom half
        pixels.set(temp, bottomOffset);
      }
      return pixels;
    }

    function _generateGlobalPalette(frames) {
      // make an array the size of every possible color in every possible frame
      // that is: width * height * frames.
      let allColors = new Uint8Array(frames.length * frames[0].length);

      // put every frame one after the other in sequence.
      // this array will hold absolutely every pixel from the animation.
      // the set function on the Uint8Array works super fast tho!
      for (let f = 0; f < frames.length; f++) {
        allColors.set(frames[f], f * frames[0].length);
      }

      // quantize this massive array into 256 colors and return it!
      let colorPalette = H(allColors, 256, {
        format: 'rgba4444',
        oneBitAlpha: true
      });

      // when generating the palette, we have to leave space for 1 of the
      // indices to be a random color that does not appear anywhere in our
      // animation to use for transparency purposes. So, if the palette is full
      // (has 256 colors), we overwrite the last one with a random, fully transparent
      // color. Otherwise, we just push a new color into the palette the same way.

      // this guarantees that when using the transparency index, there are no matches
      // between some colors of the animation and the "holes" we want to dig on them,
      // which would cause pieces of some frames to be transparent and thus look glitchy.
      if (colorPalette.length === 256) {
        colorPalette[colorPalette.length - 1] = [
          Math.random() * 255,
          Math.random() * 255,
          Math.random() * 255,
          0
        ];
      } else {
        colorPalette.push([
          Math.random() * 255,
          Math.random() * 255,
          Math.random() * 255,
          0
        ]);
      }
      return colorPalette;
    }

    /**
     * Helper function for loading GIF-based images
     */
    async function _createGif(arrayBuffer, pImg) {
      // TODO: Replace with ImageDecoder once it is widely available
      // https://developer.mozilla.org/en-US/docs/Web/API/ImageDecoder
      const gifReader = new GifReader_1(arrayBuffer);
      pImg.width = pImg.canvas.width = gifReader.width;
      pImg.height = pImg.canvas.height = gifReader.height;
      const frames = [];
      const numFrames = gifReader.numFrames();
      let framePixels = new Uint8ClampedArray(pImg.width * pImg.height * 4);

      const loadGIFFrameIntoImage = (frameNum, gifReader) => {
        try {
          gifReader.decodeAndBlitFrameRGBA(frameNum, framePixels);
        } catch (e) {
          p5._friendlyFileLoadError(8, pImg.src);
          throw e;
        }
      };

      for (let j = 0; j < numFrames; j++) {
        const frameInfo = gifReader.frameInfo(j);
        const prevFrameData = pImg.drawingContext.getImageData(
          0,
          0,
          pImg.width,
          pImg.height
        );
        framePixels = prevFrameData.data.slice();
        loadGIFFrameIntoImage(j, gifReader);
        const imageData = new ImageData(framePixels, pImg.width, pImg.height);
        pImg.drawingContext.putImageData(imageData, 0, 0);
        let frameDelay = frameInfo.delay;
        // To maintain the default of 10FPS when frameInfo.delay equals to 0
        if (frameDelay === 0) {
          frameDelay = 10;
        }
        frames.push({
          image: pImg.drawingContext.getImageData(0, 0, pImg.width, pImg.height),
          delay: frameDelay * 10 //GIF stores delay in one-hundredth of a second, shift to ms
        });

        // Some GIFs are encoded so that they expect the previous frame
        // to be under the current frame. This can occur at a sub-frame level
        //
        // Values :    0 -   No disposal specified. The decoder is
        //                   not required to take any action.
        //             1 -   Do not dispose. The graphic is to be left
        //                   in place.
        //             2 -   Restore to background color. The area used by the
        //                   graphic must be restored to the background color.
        //             3 -   Restore to previous. The decoder is required to
        //                   restore the area overwritten by the graphic with
        //                   what was there prior to rendering the graphic.
        //          4-7 -    To be defined.
        if (frameInfo.disposal === 2) {
          // Restore background color
          pImg.drawingContext.clearRect(
            frameInfo.x,
            frameInfo.y,
            frameInfo.width,
            frameInfo.height
          );
        } else if (frameInfo.disposal === 3) {
          // Restore previous
          pImg.drawingContext.putImageData(
            prevFrameData,
            0,
            0,
            frameInfo.x,
            frameInfo.y,
            frameInfo.width,
            frameInfo.height
          );
        }
      }

      //Uses Netscape block encoding
      //to repeat forever, this will be 0
      //to repeat just once, this will be null
      //to repeat N times (1<N), should contain integer for loop number
      //this is changed to more usable values for us
      //to repeat forever, loopCount = null
      //everything else is just the number of loops
      let loopLimit = gifReader.loopCount();
      if (loopLimit === null) {
        loopLimit = 1;
      } else if (loopLimit === 0) {
        loopLimit = null;
      }

      // we used the pImg for painting and saving during load
      // so we have to reset it to the first frame
      pImg.drawingContext.putImageData(frames[0].image, 0, 0);

      if (frames.length > 1) {
        pImg.gifProperties = {
          displayIndex: 0,
          loopLimit,
          loopCount: 0,
          frames,
          numFrames,
          playing: true,
          timeDisplayed: 0,
          lastChangeTime: 0
        };
      }

      return pImg;
    }

    /**
     * @private
     * @param {(LEFT|RIGHT|CENTER)} xAlign either LEFT, RIGHT or CENTER
     * @param {(TOP|BOTTOM|CENTER)} yAlign either TOP, BOTTOM or CENTER
     * @param {Number} dx
     * @param {Number} dy
     * @param {Number} dw
     * @param {Number} dh
     * @param {Number} sw
     * @param {Number} sh
     * @returns {Object}
     */

    function _imageContain(xAlign, yAlign, dx, dy, dw, dh, sw, sh) {
      const r = Math.max(sw / dw, sh / dh);
      const [adjusted_dw, adjusted_dh] = [sw / r, sh / r];
      let x = dx;
      let y = dy;

      if (xAlign === CENTER) {
        x += (dw - adjusted_dw) / 2;
      } else if (xAlign === RIGHT) {
        x += dw - adjusted_dw;
      }

      if (yAlign === CENTER) {
        y += (dh - adjusted_dh) / 2;
      } else if (yAlign === BOTTOM) {
        y += dh - adjusted_dh;
      }
      return { x, y, w: adjusted_dw, h: adjusted_dh };
    }

    /**
     * @private
     * @param {(LEFT|RIGHT|CENTER)} xAlign either LEFT, RIGHT or CENTER
     * @param {(TOP|BOTTOM|CENTER)} yAlign either TOP, BOTTOM or CENTER
     * @param {Number} dw
     * @param {Number} dh
     * @param {Number} sx
     * @param {Number} sy
     * @param {Number} sw
     * @param {Number} sh
     * @returns {Object}
     */
    function _imageCover(xAlign, yAlign, dw, dh, sx, sy, sw, sh) {
      const r = Math.max(dw / sw, dh / sh);
      const [adjusted_sw, adjusted_sh] = [dw / r, dh / r];

      let x = sx;
      let y = sy;

      if (xAlign === CENTER) {
        x += (sw - adjusted_sw) / 2;
      } else if (xAlign === RIGHT) {
        x += sw - adjusted_sw;
      }

      if (yAlign === CENTER) {
        y += (sh - adjusted_sh) / 2;
      } else if (yAlign === BOTTOM) {
        y += sh - adjusted_sh;
      }

      return { x, y, w: adjusted_sw, h: adjusted_sh };
    }

    /**
     * @private
     * @param {(CONTAIN|COVER)} [fit] either CONTAIN or COVER
     * @param {(LEFT|RIGHT|CENTER)} xAlign either LEFT, RIGHT or CENTER
     * @param {(TOP|BOTTOM|CENTER)} yAlign either TOP, BOTTOM or CENTER
     * @param {Number} dx
     * @param {Number} dy
     * @param {Number} dw
     * @param {Number} dh
     * @param {Number} sx
     * @param {Number} sy
     * @param {Number} sw
     * @param {Number} sh
     * @returns {Object}
     */
    function _imageFit(fit, xAlign, yAlign, dx, dy, dw, dh, sx, sy, sw, sh) {
      if (fit === COVER) {
        const { x, y, w, h } = _imageCover(
          xAlign, yAlign,
          dw, dh,
          sx, sy,
          sw, sh
        );
        sx = x;
        sy = y;
        sw = w;
        sh = h;
      }

      if (fit === CONTAIN) {
        const { x, y, w, h } = _imageContain(
          xAlign,
          yAlign,
          dx,
          dy,
          dw,
          dh,
          sw,
          sh
        );
        dx = x;
        dy = y;
        dw = w;
        dh = h;
      }
      return { sx, sy, sw, sh, dx, dy, dw, dh };
    }

    /**
     * Validates clipping params. Per drawImage spec sWidth and sHight cannot be
     * negative or greater than image intrinsic width and height
     * @private
     * @param {Number} sVal
     * @param {Number} iVal
     * @returns {Number}
     * @private
     */
    function _sAssign(sVal, iVal) {
      if (sVal > 0 && sVal < iVal) {
        return sVal;
      } else {
        return iVal;
      }
    }

    /**
     * Draws an image to the canvas.
     *
     * The first parameter, `img`, is the source image to be drawn. `img` can be
     * any of the following objects:
     * - <a href="#/p5.Image">p5.Image</a>
     * - <a href="#/p5.Element">p5.Element</a>
     * - <a href="#/p5.Texture">p5.Texture</a>
     * - <a href="#/p5.Framebuffer">p5.Framebuffer</a>
     * - <a href="#/p5.FramebufferTexture">p5.FramebufferTexture</a>
     *
     * The second and third parameters, `dx` and `dy`, set the coordinates of the
     * destination image's top left corner. See
     * <a href="#/p5/imageMode">imageMode()</a> for other ways to position images.
     *
     * ```js example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/laDefense.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Draw the image.
     *   image(img, 0, 0);
     *
     *   describe('An image of the underside of a white umbrella with a gridded ceiling above.');
     * }
     * ```
     *
     * Here's a diagram that explains how optional parameters work in `image()`:
     *
     * <img src="assets/drawImage.png"></img>
     *
     * The fourth and fifth parameters, `dw` and `dh`, are optional. They set the
     * the width and height to draw the destination image. By default, `image()`
     * draws the full source image at its original size.
     *
     * The sixth and seventh parameters, `sx` and `sy`, are also optional.
     * These coordinates define the top left corner of a subsection to draw from
     * the source image.
     *
     * The eighth and ninth parameters, `sw` and `sh`, are also optional.
     * They define the width and height of a subsection to draw from the source
     * image. By default, `image()` draws the full subsection that begins at
     * `(sx, sy)` and extends to the edges of the source image.
     *
     * The ninth parameter, `fit`, is also optional. It enables a subsection of
     * the source image to be drawn without affecting its aspect ratio. If
     * `CONTAIN` is passed, the full subsection will appear within the destination
     * rectangle. If `COVER` is passed, the subsection will completely cover the
     * destination rectangle. This may have the effect of zooming into the
     * subsection.
     *
     * The tenth and eleventh paremeters, `xAlign` and `yAlign`, are also
     * optional. They determine how to align the fitted subsection. `xAlign` can
     * be set to either `LEFT`, `RIGHT`, or `CENTER`. `yAlign` can be set to
     * either `TOP`, `BOTTOM`, or `CENTER`. By default, both `xAlign` and `yAlign`
     * are set to `CENTER`.
     *
     * @method image
     * @param  {p5.Image|p5.Element|p5.Texture|p5.Framebuffer|p5.FramebufferTexture|p5.Renderer|p5.Graphics} img image to display.
     * @param  {Number}   x x-coordinate of the top-left corner of the image.
     * @param  {Number}   y y-coordinate of the top-left corner of the image.
     * @param  {Number}   [width]  width to draw the image.
     * @param  {Number}   [height] height to draw the image.
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/laDefense.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Draw the image.
     *   image(img, 10, 10);
     *
     *   describe('An image of the underside of a white umbrella with a gridded ceiling above. The image has dark gray borders on its left and top.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/laDefense.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Draw the image 50x50.
     *   image(img, 0, 0, 50, 50);
     *
     *   describe('An image of the underside of a white umbrella with a gridded ceiling above. The image is drawn in the top left corner of a dark gray square.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/laDefense.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Draw the center of the image.
     *   image(img, 25, 25, 50, 50, 25, 25, 50, 50);
     *
     *   describe('An image of a gridded ceiling drawn in the center of a dark gray square.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/moonwalk.jpg');
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Draw the image and scale it to fit within the canvas.
     *   image(img, 0, 0, width, height, 0, 0, img.width, img.height, CONTAIN);
     *
     *   describe('An image of an astronaut on the moon. The top and bottom borders of the image are dark gray.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/laDefense50.png');
     *
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Draw the image and scale it to cover the canvas.
     *   image(img, 0, 0, width, height, 0, 0, img.width, img.height, COVER);
     *
     *   describe('A pixelated image of the underside of a white umbrella with a gridded ceiling above.');
     * }
     */
    /**
     * @method image
     * @param  {p5.Image|p5.Element|p5.Texture|p5.Framebuffer|p5.FramebufferTexture} img
     * @param  {Number}   dx     the x-coordinate of the destination
     *                           rectangle in which to draw the source image
     * @param  {Number}   dy     the y-coordinate of the destination
     *                           rectangle in which to draw the source image
     * @param  {Number}   dWidth  the width of the destination rectangle
     * @param  {Number}   dHeight the height of the destination rectangle
     * @param  {Number}   sx     the x-coordinate of the subsection of the source
     * image to draw into the destination rectangle
     * @param  {Number}   sy     the y-coordinate of the subsection of the source
     * image to draw into the destination rectangle
     * @param {Number}    [sWidth] the width of the subsection of the
     *                           source image to draw into the destination
     *                           rectangle
     * @param {Number}    [sHeight] the height of the subsection of the
     *                            source image to draw into the destination rectangle
     * @param {(CONTAIN|COVER)} [fit] either CONTAIN or COVER
     * @param {(LEFT|RIGHT|CENTER)} [xAlign=CENTER] either LEFT, RIGHT or CENTER default is CENTER
     * @param {(TOP|BOTTOM|CENTER)} [yAlign=CENTER] either TOP, BOTTOM or CENTER default is CENTER
     */
    fn.image = function(
      img,
      dx,
      dy,
      dWidth,
      dHeight,
      sx,
      sy,
      sWidth,
      sHeight,
      fit,
      xAlign,
      yAlign
    ) {
      // set defaults per spec: https://goo.gl/3ykfOq

      // p5._validateParameters('image', arguments);

      let defW = img.width;
      let defH = img.height;
      yAlign = yAlign || CENTER;
      xAlign = xAlign || CENTER;

      if (img.elt) {
        defW = defW !== undefined ? defW : img.elt.width;
        defH = defH !== undefined ? defH : img.elt.height;
      }
      if (img.elt && img.elt.videoWidth && !img.canvas) {
        // video no canvas
        defW = defW !== undefined ? defW : img.elt.videoWidth;
        defH = defH !== undefined ? defH : img.elt.videoHeight;
      }

      let _dx = dx;
      let _dy = dy;
      let _dw = dWidth || defW;
      let _dh = dHeight || defH;
      let _sx = sx || 0;
      let _sy = sy || 0;
      let _sw = sWidth !== undefined ? sWidth : defW;
      let _sh = sHeight !== undefined ? sHeight : defH;

      _sw = _sAssign(_sw, defW);
      _sh = _sAssign(_sh, defH);

      // This part needs cleanup and unit tests
      // see issues https://github.com/processing/p5.js/issues/1741
      // and https://github.com/processing/p5.js/issues/1673
      let pd = 1;

      if (img.elt && !img.canvas && img.elt.style.width) {
        //if img is video and img.elt.size() has been used and
        //no width passed to image()
        if (img.elt.videoWidth && !dWidth) {
          pd = img.elt.videoWidth;
        } else {
          //all other cases
          pd = img.elt.width;
        }
        pd /= parseInt(img.elt.style.width, 10);
      }

      _sx *= pd;
      _sy *= pd;
      _sh *= pd;
      _sw *= pd;

      let vals = canvas.modeAdjust(
        _dx, _dy,
        _dw, _dh,
        this._renderer.states.imageMode
      );
      vals = _imageFit(
        fit,
        xAlign,
        yAlign,
        vals.x,
        vals.y,
        vals.w,
        vals.h,
        _sx,
        _sy,
        _sw,
        _sh
      );

      // tint the image if there is a tint
      this._renderer.image(
        img,
        vals.sx,
        vals.sy,
        vals.sw,
        vals.sh,
        vals.dx,
        vals.dy,
        vals.dw,
        vals.dh
      );
    };

    /**
     * Tints images using a color.
     *
     * The version of `tint()` with one parameter interprets it one of four ways.
     * If the parameter is a number, it's interpreted as a grayscale value. If the
     * parameter is a string, it's interpreted as a CSS color string. An array of
     * `[R, G, B, A]` values or a <a href="#/p5.Color">p5.Color</a> object can
     * also be used to set the tint color.
     *
     * The version of `tint()` with two parameters uses the first one as a
     * grayscale value and the second as an alpha value. For example, calling
     * `tint(255, 128)` will make an image 50% transparent.
     *
     * The version of `tint()` with three parameters interprets them as RGB or
     * HSB values, depending on the current
     * <a href="#/p5/colorMode">colorMode()</a>. The optional fourth parameter
     * sets the alpha value. For example, `tint(255, 0, 0, 100)` will give images
     * a red tint and make them transparent.
     *
     * @method tint
     * @param  {Number}        v1      red or hue value.
     * @param  {Number}        v2      green or saturation value.
     * @param  {Number}        v3      blue or brightness.
     * @param  {Number}        [alpha]
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/laDefense.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Left image.
     *   image(img, 0, 0);
     *
     *   // Right image.
     *   // Tint with a CSS color string.
     *   tint('red');
     *   image(img, 50, 0);
     *
     *   describe('Two images of an umbrella and a ceiling side-by-side. The image on the right has a red tint.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/laDefense.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Left image.
     *   image(img, 0, 0);
     *
     *   // Right image.
     *   // Tint with RGB values.
     *   tint(255, 0, 0);
     *   image(img, 50, 0);
     *
     *   describe('Two images of an umbrella and a ceiling side-by-side. The image on the right has a red tint.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/laDefense.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Left.
     *   image(img, 0, 0);
     *
     *   // Right.
     *   // Tint with RGBA values.
     *   tint(255, 0, 0, 100);
     *   image(img, 50, 0);
     *
     *   describe('Two images of an umbrella and a ceiling side-by-side. The image on the right has a transparent red tint.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/laDefense.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Left.
     *   image(img, 0, 0);
     *
     *   // Right.
     *   // Tint with grayscale and alpha values.
     *   tint(255, 180);
     *   image(img, 50, 0);
     *
     *   describe('Two images of an umbrella and a ceiling side-by-side. The image on the right is transparent.');
     * }
     */
    /**
     * @method tint
     * @param  {String}        value   CSS color string.
     */
    /**
     * @method tint
     * @param  {Number}        gray   grayscale value.
     * @param  {Number}        [alpha]
     */
    /**
     * @method tint
     * @param  {Number[]}      values  array containing the red, green, blue &
     *                                 alpha components of the color.
     */
    /**
     * @method tint
     * @param  {p5.Color}      color   the tint color
     */
    fn.tint = function(...args) {
      // p5._validateParameters('tint', args);
      const c = this.color(...args);
      this._renderer.states.setValue('tint', c._getRGBA([255, 255, 255, 255]));
    };

    /**
     * Removes the current tint set by <a href="#/p5/tint">tint()</a>.
     *
     * `noTint()` restores images to their original colors.
     *
     * @method noTint
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/laDefense.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Left.
     *   // Tint with a CSS color string.
     *   tint('red');
     *   image(img, 0, 0);
     *
     *   // Right.
     *   // Remove the tint.
     *   noTint();
     *   image(img, 50, 0);
     *
     *   describe('Two images of an umbrella and a ceiling side-by-side. The image on the left has a red tint.');
     * }
     */
    fn.noTint = function() {
      this._renderer.states.setValue('tint', null);
    };

    /**
     * Apply the current tint color to the input image, return the resulting
     * canvas.
     *
     * @private
     * @param {p5.Image} The image to be tinted
     * @return {canvas} The resulting tinted canvas
     */
    // fn._getTintedImageCanvas =
    //   p5.Renderer2D.prototype._getTintedImageCanvas;

    /**
     * Changes the location from which images are drawn when
     * <a href="#/p5/image">image()</a> is called.
     *
     * By default, the first
     * two parameters of <a href="#/p5/image">image()</a> are the x- and
     * y-coordinates of the image's upper-left corner. The next parameters are
     * its width and height. This is the same as calling `imageMode(CORNER)`.
     *
     * `imageMode(CORNERS)` also uses the first two parameters of
     * <a href="#/p5/image">image()</a> as the x- and y-coordinates of the image's
     * top-left corner. The third and fourth parameters are the coordinates of its
     * bottom-right corner.
     *
     * `imageMode(CENTER)` uses the first two parameters of
     * <a href="#/p5/image">image()</a> as the x- and y-coordinates of the image's
     * center. The next parameters are its width and height.
     *
     * @method imageMode
     * @param {(CORNER|CORNERS|CENTER)} mode either CORNER, CORNERS, or CENTER.
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use CORNER mode.
     *   imageMode(CORNER);
     *
     *   // Display the image.
     *   image(img, 10, 10, 50, 50);
     *
     *   describe('A square image of a brick wall is drawn at the top left of a gray square.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use CORNERS mode.
     *   imageMode(CORNERS);
     *
     *   // Display the image.
     *   image(img, 10, 10, 90, 40);
     *
     *   describe('An image of a brick wall is drawn on a gray square. The image is squeezed into a small rectangular area.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use CENTER mode.
     *   imageMode(CENTER);
     *
     *   // Display the image.
     *   image(img, 50, 50, 80, 80);
     *
     *   describe('A square image of a brick wall is drawn on a gray square.');
     * }
     */
    fn.imageMode = function(m) {
      // p5._validateParameters('imageMode', arguments);
      if (
        m === CORNER ||
        m === CORNERS ||
        m === CENTER
      ) {
        this._renderer.states.setValue('imageMode', m);
      }
    };
  }

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

  /**
   * @module Image
   * @submodule Pixels
   * @for p5
   * @requires core
   */


  function pixels(p5, fn){
    /**
     * Copies a region of pixels from one image to another.
     *
     * The first parameter, `srcImage`, is the
     * <a href="#/p5.Image">p5.Image</a> object to blend.
     *
     * The next four parameters, `sx`, `sy`, `sw`, and `sh` determine the region
     * to blend from the source image. `(sx, sy)` is the top-left corner of the
     * region. `sw` and `sh` are the regions width and height.
     *
     * The next four parameters, `dx`, `dy`, `dw`, and `dh` determine the region
     * of the canvas to blend into. `(dx, dy)` is the top-left corner of the
     * region. `dw` and `dh` are the regions width and height.
     *
     * The tenth parameter, `blendMode`, sets the effect used to blend the images'
     * colors. The options are `BLEND`, `DARKEST`, `LIGHTEST`, `DIFFERENCE`,
     * `MULTIPLY`, `EXCLUSION`, `SCREEN`, `REPLACE`, `OVERLAY`, `HARD_LIGHT`,
     * `SOFT_LIGHT`, `DODGE`, `BURN`, `ADD`, or `NORMAL`
     *
     * @method blend
     * @param  {p5.Image} srcImage source image.
     * @param  {Integer} sx x-coordinate of the source's upper-left corner.
     * @param  {Integer} sy y-coordinate of the source's upper-left corner.
     * @param  {Integer} sw source image width.
     * @param  {Integer} sh source image height.
     * @param  {Integer} dx x-coordinate of the destination's upper-left corner.
     * @param  {Integer} dy y-coordinate of the destination's upper-left corner.
     * @param  {Integer} dw destination image width.
     * @param  {Integer} dh destination image height.
     * @param  {(BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL)} blendMode the blend mode. either
     *     BLEND, DARKEST, LIGHTEST, DIFFERENCE,
     *     MULTIPLY, EXCLUSION, SCREEN, REPLACE, OVERLAY, HARD_LIGHT,
     *     SOFT_LIGHT, DODGE, BURN, ADD or NORMAL.
     *
     * @example
     * let img0;
     * let img1;
     *
     * async function setup() {
     *   // Load the images.
     *   img0 = await loadImage('assets/rockies.jpg');
     *   img1 = await loadImage('assets/bricks_third.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Use the mountains as the background.
     *   background(img0);
     *
     *   // Display the bricks.
     *   image(img1, 0, 0);
     *
     *   // Display the bricks faded into the landscape.
     *   blend(img1, 0, 0, 33, 100, 67, 0, 33, 100, LIGHTEST);
     *
     *   describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears faded on the right of the image.');
     * }
     *
     * @example
     * let img0;
     * let img1;
     *
     * async function setup() {
     *   // Load the images.
     *   img0 = await loadImage('assets/rockies.jpg');
     *   img1 = await loadImage('assets/bricks_third.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Use the mountains as the background.
     *   background(img0);
     *
     *   // Display the bricks.
     *   image(img1, 0, 0);
     *
     *   // Display the bricks partially transparent.
     *   blend(img1, 0, 0, 33, 100, 67, 0, 33, 100, DARKEST);
     *
     *   describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears transparent on the right of the image.');
     * }
     *
     * @example
     * let img0;
     * let img1;
     *
     * async function setup() {
     *   // Load the images.
     *   img0 = await loadImage('assets/rockies.jpg');
     *   img1 = await loadImage('assets/bricks_third.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Use the mountains as the background.
     *   background(img0);
     *
     *   // Display the bricks.
     *   image(img1, 0, 0);
     *
     *   // Display the bricks washed out into the landscape.
     *   blend(img1, 0, 0, 33, 100, 67, 0, 33, 100, ADD);
     *
     *   describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears washed out on the right of the image.');
     * }
     */
    /**
     * @method blend
     * @param  {Integer} sx
     * @param  {Integer} sy
     * @param  {Integer} sw
     * @param  {Integer} sh
     * @param  {Integer} dx
     * @param  {Integer} dy
     * @param  {Integer} dw
     * @param  {Integer} dh
     * @param  {(BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL)} blendMode
     */
    fn.blend = function(...args) {
      // p5._validateParameters('blend', args);
      if (this._renderer) {
        this._renderer.blend(...args);
      } else {
        p5.Renderer2D.prototype.blend.apply(this, args);
      }
    };

    /**
     * Copies pixels from a source image to a region of the canvas.
     *
     * The first parameter, `srcImage`, is the
     * <a href="#/p5.Image">p5.Image</a> object to blend. The source image can be
     * the canvas itself or a
     * <a href="#/p5.Image">p5.Image</a> object. `copy()` will scale pixels from
     * the source region if it isn't the same size as the destination region.
     *
     * The next four parameters, `sx`, `sy`, `sw`, and `sh` determine the region
     * to copy from the source image. `(sx, sy)` is the top-left corner of the
     * region. `sw` and `sh` are the region's width and height.
     *
     * The next four parameters, `dx`, `dy`, `dw`, and `dh` determine the region
     * of the canvas to copy into. `(dx, dy)` is the top-left corner of the
     * region. `dw` and `dh` are the region's width and height.
     *
     * @method copy
     * @param  {p5.Image|p5.Element} srcImage source image.
     * @param  {Integer} sx x-coordinate of the source's upper-left corner.
     * @param  {Integer} sy y-coordinate of the source's upper-left corner.
     * @param  {Integer} sw source image width.
     * @param  {Integer} sh source image height.
     * @param  {Integer} dx x-coordinate of the destination's upper-left corner.
     * @param  {Integer} dy y-coordinate of the destination's upper-left corner.
     * @param  {Integer} dw destination image width.
     * @param  {Integer} dh destination image height.
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/rockies.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Use the mountains as the background.
     *   background(img);
     *
     *   // Copy a region of pixels to another spot.
     *   copy(img, 7, 22, 10, 10, 35, 25, 50, 50);
     *
     *   // Outline the copied region.
     *   stroke(255);
     *   noFill();
     *   square(7, 22, 10);
     *
     *   describe('An image of a mountain landscape. A square region is outlined in white. A larger square contains a pixelated view of the outlined region.');
     * }
     */
    /**
     * @method copy
     * @param  {Integer} sx
     * @param  {Integer} sy
     * @param  {Integer} sw
     * @param  {Integer} sh
     * @param  {Integer} dx
     * @param  {Integer} dy
     * @param  {Integer} dw
     * @param  {Integer} dh
     */
    fn.copy = function(...args) {
      let srcImage, sx, sy, sw, sh, dx, dy, dw, dh;
      if (args.length === 9) {
        srcImage = args[0];
        sx = args[1];
        sy = args[2];
        sw = args[3];
        sh = args[4];
        dx = args[5];
        dy = args[6];
        dw = args[7];
        dh = args[8];
      } else if (args.length === 8) {
        srcImage = this;
        sx = args[0];
        sy = args[1];
        sw = args[2];
        sh = args[3];
        dx = args[4];
        dy = args[5];
        dw = args[6];
        dh = args[7];
      } else {
        throw new Error('Signature not supported');
      }

      fn._copyHelper(this, srcImage, sx, sy, sw, sh, dx, dy, dw, dh);
    };

    fn._copyHelper = (
      dstImage,
      srcImage,
      sx,
      sy,
      sw,
      sh,
      dx,
      dy,
      dw,
      dh
    ) => {
      const s = srcImage.canvas.width / srcImage.width;
      // adjust coord system for 3D when renderer
      // ie top-left = -width/2, -height/2
      let sxMod = 0;
      let syMod = 0;
      if (srcImage._renderer && srcImage._renderer.isP3D) {
        sxMod = srcImage.width / 2;
        syMod = srcImage.height / 2;
      }
      if (dstImage._renderer && dstImage._renderer.isP3D) {
        dstImage.push();
        dstImage.resetMatrix();
        dstImage.noLights();
        dstImage.blendMode(dstImage.BLEND);
        dstImage.imageMode(dstImage.CORNER);
        dstImage._renderer.image(
          srcImage,
          sx + sxMod,
          sy + syMod,
          sw,
          sh,
          dx,
          dy,
          dw,
          dh
        );
        dstImage.pop();
      } else {
        dstImage.drawingContext.drawImage(
          srcImage.canvas,
          s * (sx + sxMod),
          s * (sy + syMod),
          s * sw,
          s * sh,
          dx,
          dy,
          dw,
          dh
        );
      }
    };

    /**
     * Applies an image filter to the canvas.
     *
     * The preset options are:
     *
     * `INVERT`
     * Inverts the colors in the image. No parameter is used.
     *
     * `GRAY`
     * Converts the image to grayscale. No parameter is used.
     *
     * `THRESHOLD`
     * Converts the image to black and white. Pixels with a grayscale value
     * above a given threshold are converted to white. The rest are converted to
     * black. The threshold must be between 0.0 (black) and 1.0 (white). If no
     * value is specified, 0.5 is used.
     *
     * `OPAQUE`
     * Sets the alpha channel to entirely opaque. No parameter is used.
     *
     * `POSTERIZE`
     * Limits the number of colors in the image. Each color channel is limited to
     * the number of colors specified. Values between 2 and 255 are valid, but
     * results are most noticeable with lower values. The default value is 4.
     *
     * `BLUR`
     * Blurs the image. The level of blurring is specified by a blur radius. Larger
     * values increase the blur. The default value is 4. A gaussian blur is used
     * in `P2D` mode. A box blur is used in `WEBGL` mode.
     *
     * `ERODE`
     * Reduces the light areas. No parameter is used.
     *
     * `DILATE`
     * Increases the light areas. No parameter is used.
     *
     * `filter()` uses WebGL in the background by default because it's faster.
     * This can be disabled in `P2D` mode by adding a `false` argument, as in
     * `filter(BLUR, false)`. This may be useful to keep computation off the GPU
     * or to work around a lack of WebGL support.
     *
     * In WebgL mode, `filter()` can also use custom shaders. See
     * <a href="#/p5/createFilterShader">createFilterShader()</a> for more
     * information.
     *
     *
     * @method filter
     * @param  {(THRESHOLD|GRAY|OPAQUE|INVERT|POSTERIZE|BLUR|ERODE|DILATE|BLUR)} filterType  either THRESHOLD, GRAY, OPAQUE, INVERT,
     *                                POSTERIZE, BLUR, ERODE, DILATE or BLUR.
     * @param  {Number} [filterParam] parameter unique to each filter.
     * @param  {Boolean} [useWebGL=true]   flag to control whether to use fast
     *                                WebGL filters (GPU) or original image
     *                                filters (CPU); defaults to `true`.
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Apply the INVERT filter.
     *   filter(INVERT);
     *
     *   describe('A blue brick wall.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Apply the GRAY filter.
     *   filter(GRAY);
     *
     *   describe('A brick wall drawn in grayscale.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Apply the THRESHOLD filter.
     *   filter(THRESHOLD);
     *
     *   describe('A brick wall drawn in black and white.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Apply the OPAQUE filter.
     *   filter(OPAQUE);
     *
     *   describe('A red brick wall.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Apply the POSTERIZE filter.
     *   filter(POSTERIZE, 3);
     *
     *   describe('An image of a red brick wall drawn with limited color palette.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Apply the BLUR filter.
     *   filter(BLUR, 3);
     *
     *   describe('A blurry image of a red brick wall.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Apply the DILATE filter.
     *   filter(DILATE);
     *
     *   describe('A red brick wall with bright lines between each brick.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Apply the ERODE filter.
     *   filter(ERODE);
     *
     *   describe('A red brick wall with faint lines between each brick.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/bricks.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Apply the BLUR filter.
     *   // Don't use WebGL.
     *   filter(BLUR, 3, false);
     *
     *   describe('A blurry image of a red brick wall.');
     * }
     */

    /**
     * @method getFilterGraphicsLayer
     * @private
     * @returns {p5.Graphics}
     */
    fn.getFilterGraphicsLayer = function() {
      return this._renderer.getFilterGraphicsLayer();
    };

    /**
     * @method filter
     * @param  {(THRESHOLD|GRAY|OPAQUE|INVERT|POSTERIZE|BLUR|ERODE|DILATE|BLUR)} filterType
     * @param  {Number} [filterParam]
     * @param  {Boolean} [useWebGL=true]
     */
    /**
     * @method filter
     * @param {p5.Shader}  shaderFilter  shader that's been loaded, with the
     *                                   frag shader using a `tex0` uniform.
     */
    fn.filter = function(...args) {
      // p5._validateParameters('filter', args);

      let { shader, operation, value, useWebGL } = parseFilterArgs(...args);

      // when passed a shader, use it directly
      if (this._renderer.isP3D && shader) {
        this._renderer.filter(shader);
        return;
      }

      // when opting out of webgl, use old pixels method
      if (!useWebGL && !this._renderer.isP3D) {
        if (this.canvas !== undefined) {
          Filters.apply(this.canvas, Filters[operation], value);
        } else {
          Filters.apply(this.elt, Filters[operation], value);
        }
        return;
      }

      if(!useWebGL && this._renderer.isP3D) {
        console.warn('filter() with useWebGL=false is not supported in WEBGL');
      }

      // when this is a webgl renderer, apply constant shader filter
      if (this._renderer.isP3D) {
        this._renderer.filter(operation, value);
      }

      // when this is P2D renderer, create/use hidden webgl renderer
      else {

        if (shader) {
          this._renderer.filterRenderer.setOperation(operation, value, shader);
        } else {
          this._renderer.filterRenderer.setOperation(operation, value);
        }

        this._renderer.filterRenderer.applyFilter();
      }
    };

    function parseFilterArgs(...args) {
      // args could be:
      // - operation, value, [useWebGL]
      // - operation, [useWebGL]
      // - shader

      let result = {
        shader: undefined,
        operation: undefined,
        value: undefined,
        useWebGL: true
      };

      if (args[0] instanceof p5.Shader) {
        result.shader = args[0];
        return result;
      }
      else {
        result.operation = args[0];
      }

      if (args.length > 1 && typeof args[1] === 'number') {
        result.value = args[1];
      }

      if (args[args.length-1] === false) {
        result.useWebGL = false;
      }
      return result;
    }

    /**
     * Gets a pixel or a region of pixels from the canvas.
     *
     * `get()` is easy to use but it's not as fast as
     * <a href="#/p5/pixels">pixels</a>. Use <a href="#/p5/pixels">pixels</a>
     * to read many pixel values.
     *
     * The version of `get()` with no parameters returns the entire canvas.
     *
     * The version of `get()` with two parameters interprets them as
     * coordinates. It returns an array with the `[R, G, B, A]` values of the
     * pixel at the given point.
     *
     * The version of `get()` with four parameters interprets them as coordinates
     * and dimensions. It returns a subsection of the canvas as a
     * <a href="#/p5.Image">p5.Image</a> object. The first two parameters are the
     * coordinates for the upper-left corner of the subsection. The last two
     * parameters are the width and height of the subsection.
     *
     * Use <a href="#/p5.Image/get">p5.Image.get()</a> to work directly with
     * <a href="#/p5.Image">p5.Image</a> objects.
     *
     * @method get
     * @param  {Number}         x x-coordinate of the pixel.
     * @param  {Number}         y y-coordinate of the pixel.
     * @param  {Number}         w width of the subsection to be returned.
     * @param  {Number}         h height of the subsection to be returned.
     * @return {p5.Image}       subsection as a <a href="#/p5.Image">p5.Image</a> object.
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/rockies.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Get the entire canvas.
     *   let c = get();
     *
     *   // Display half the canvas.
     *   image(c, 50, 0);
     *
     *   describe('Two identical mountain landscapes shown side-by-side.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/rockies.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Get the color of a pixel.
     *   let c = get(50, 90);
     *
     *   // Style the square with the pixel's color.
     *   fill(c);
     *   noStroke();
     *
     *   // Display the square.
     *   square(25, 25, 50);
     *
     *   describe('A mountain landscape with an olive green square in its center.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/rockies.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0);
     *
     *   // Get a region of the image.
     *   let c = get(0, 0, 50, 50);
     *
     *   // Display the region.
     *   image(c, 50, 50);
     *
     *   describe('A mountain landscape drawn on top of another mountain landscape.');
     * }
     */
    /**
     * @method get
     * @return {p5.Image}      whole canvas as a <a href="#/p5.Image">p5.Image</a>.
     */
    /**
     * @method get
     * @param  {Number}        x
     * @param  {Number}        y
     * @return {Number[]}      color of the pixel at (x, y) in array format `[R, G, B, A]`.
     */
    fn.get = function(x, y, w, h) {
      // p5._validateParameters('get', arguments);
      return this._renderer.get(...arguments);
    };

    /**
     * Loads the current value of each pixel on the canvas into the
     * <a href="#/p5/pixels">pixels</a> array.
     *
     * `loadPixels()` must be called before reading from or writing to
     * <a href="#/p5/pixels">pixels</a>.
     *
     * @method loadPixels
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/rockies.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0, 100, 100);
     *
     *   // Get the pixel density.
     *   let d = pixelDensity();
     *
     *   // Calculate the halfway index in the pixels array.
     *   let halfImage = 4 * (d * width) * (d * height / 2);
     *
     *   // Load the pixels array.
     *   loadPixels();
     *
     *   // Copy the top half of the canvas to the bottom.
     *   for (let i = 0; i < halfImage; i += 1) {
     *     pixels[i + halfImage] = pixels[i];
     *   }
     *
     *   // Update the canvas.
     *   updatePixels();
     *
     *   describe('Two identical images of mountain landscapes, one on top of the other.');
     * }
     */
    fn.loadPixels = function(...args) {
      // p5._validateParameters('loadPixels', args);
      return this._renderer.loadPixels();
    };

    /**
     * Sets the color of a pixel or draws an image to the canvas.
     *
     * `set()` is easy to use but it's not as fast as
     * <a href="#/p5/pixels">pixels</a>. Use <a href="#/p5/pixels">pixels</a>
     * to set many pixel values.
     *
     * `set()` interprets the first two parameters as x- and y-coordinates. It
     * interprets the last parameter as a grayscale value, a `[R, G, B, A]` pixel
     * array, a <a href="#/p5.Color">p5.Color</a> object, or a
     * <a href="#/p5.Image">p5.Image</a> object. If an image is passed, the first
     * two parameters set the coordinates for the image's upper-left corner,
     * regardless of the current <a href="#/p5/imageMode">imageMode()</a>.
     *
     * <a href="#/p5/updatePixels">updatePixels()</a> must be called after using
     * `set()` for changes to appear.
     *
     * @method set
     * @param {Number}              x x-coordinate of the pixel.
     * @param {Number}              y y-coordinate of the pixel.
     * @param {Number|Number[]|Object} c grayscale value | pixel array |
     *                                <a href="#/p5.Color">p5.Color</a> object | <a href="#/p5.Image">p5.Image</a> to copy.
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set four pixels to black.
     *   set(30, 20, 0);
     *   set(85, 20, 0);
     *   set(85, 75, 0);
     *   set(30, 75, 0);
     *
     *   // Update the canvas.
     *   updatePixels();
     *
     *   describe('Four black dots arranged in a square drawn on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a p5.Color object.
     *   let black = color(0);
     *
     *   // Set four pixels to black.
     *   set(30, 20, black);
     *   set(85, 20, black);
     *   set(85, 75, black);
     *   set(30, 75, black);
     *
     *   // Update the canvas.
     *   updatePixels();
     *
     *   describe('Four black dots arranged in a square drawn on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(255);
     *
     *   // Draw a horizontal color gradient.
     *   for (let x = 0; x < 100; x += 1) {
     *     for (let y = 0; y < 100; y += 1) {
     *       // Calculate the grayscale value.
     *       let c = map(x, 0, 100, 0, 255);
     *
     *       // Set the pixel using the grayscale value.
     *       set(x, y, c);
     *     }
     *   }
     *
     *   // Update the canvas.
     *   updatePixels();
     *
     *   describe('A horiztonal color gradient from black to white.');
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/rockies.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Use the image to set all pixels.
     *   set(0, 0, img);
     *
     *   // Update the canvas.
     *   updatePixels();
     *
     *   describe('An image of a mountain landscape.');
     * }
     */
    fn.set = function(x, y, imgOrCol) {
      this._renderer.set(x, y, imgOrCol);
    };

    /**
     * Updates the canvas with the RGBA values in the
     * <a href="#/p5/pixels">pixels</a> array.
     *
     * `updatePixels()` only needs to be called after changing values in the
     * <a href="#/p5/pixels">pixels</a> array. Such changes can be made directly
     * after calling <a href="#/p5/loadPixels">loadPixels()</a> or by calling
     * <a href="#/p5/set">set()</a>.
     *
     * @method updatePixels
     * @param  {Number} [x]    x-coordinate of the upper-left corner of region
     *                         to update.
     * @param  {Number} [y]    y-coordinate of the upper-left corner of region
     *                         to update.
     * @param  {Number} [w]    width of region to update.
     * @param  {Number} [h]    height of region to update.
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load the image.
     *   img = await loadImage('assets/rockies.jpg');
     *
     *   createCanvas(100, 100);
     *
     *   // Display the image.
     *   image(img, 0, 0, 100, 100);
     *
     *   // Get the pixel density.
     *   let d = pixelDensity();
     *
     *   // Calculate the halfway index in the pixels array.
     *   let halfImage = 4 * (d * width) * (d * height / 2);
     *
     *   // Load the pixels array.
     *   loadPixels();
     *
     *   // Copy the top half of the canvas to the bottom.
     *   for (let i = 0; i < halfImage; i += 1) {
     *     pixels[i + halfImage] = pixels[i];
     *   }
     *
     *   // Update the canvas.
     *   updatePixels();
     *
     *   describe('Two identical images of mountain landscapes, one on top of the other.');
     * }
     */
    fn.updatePixels = function(x, y, w, h) {
      // p5._validateParameters('updatePixels', arguments);
      // graceful fail - if loadPixels() or set() has not been called, pixel
      // array will be empty, ignore call to updatePixels()
      if (this.pixels.length === 0) {
        return;
      }
      this._renderer.updatePixels(x, y, w, h);
    };

    /**
     * An array containing the color of each pixel on the canvas.
     *
     * Colors are stored as numbers representing red, green, blue, and alpha
     * (RGBA) values. `pixels` is a one-dimensional array for performance reasons.
     *
     * Each pixel occupies four elements in the `pixels` array, one for each RGBA
     * value. For example, the pixel at coordinates (0, 0) stores its RGBA values
     * at `pixels[0]`, `pixels[1]`, `pixels[2]`, and `pixels[3]`, respectively.
     * The next pixel at coordinates (1, 0) stores its RGBA values at `pixels[4]`,
     * `pixels[5]`, `pixels[6]`, and `pixels[7]`. And so on. The `pixels` array
     * for a 100&times;100 canvas has 100 &times; 100 &times; 4 = 40,000 elements.
     *
     * Some displays use several smaller pixels to set the color at a single
     * point. The <a href="#/p5/pixelDensity">pixelDensity()</a> function returns
     * the pixel density of the canvas. High density displays often have a
     * <a href="#/p5/pixelDensity">pixelDensity()</a> of 2. On such a display, the
     * `pixels` array for a 100&times;100 canvas has 200 &times; 200 &times; 4 =
     * 160,000 elements.
     *
     * Accessing the RGBA values for a point on the canvas requires a little math
     * as shown below. The <a href="#/p5/loadPixels">loadPixels()</a> function
     * must be called before accessing the `pixels` array. The
     * <a href="#/p5/updatePixels">updatePixels()</a> function must be called
     * after any changes are made.
     *
     * @property {Number[]} pixels
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *   background(128);
     *
     *   // Load the pixels array.
     *   loadPixels();
     *
     *   // Set the dot's coordinates.
     *   let x = 50;
     *   let y = 50;
     *
     *   // Get the pixel density.
     *   let d = pixelDensity();
     *
     *   // Set the pixel(s) at the center of the canvas black.
     *   for (let i = 0; i < d; i += 1) {
     *     for (let j = 0; j < d; j += 1) {
     *       let index = 4 * ((y * d + j) * width * d + (x * d + i));
     *       // Red.
     *       pixels[index] = 0;
     *       // Green.
     *       pixels[index + 1] = 0;
     *       // Blue.
     *       pixels[index + 2] = 0;
     *       // Alpha.
     *       pixels[index + 3] = 255;
     *     }
     *   }
     *
     *   // Update the canvas.
     *   updatePixels();
     *
     *   describe('A black dot in the middle of a gray rectangle.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Load the pixels array.
     *   loadPixels();
     *
     *   // Get the pixel density.
     *   let d = pixelDensity();
     *
     *   // Calculate the halfway index in the pixels array.
     *   let halfImage = 4 * (d * width) * (d * height / 2);
     *
     *   // Make the top half of the canvas red.
     *   for (let i = 0; i < halfImage; i += 4) {
     *     // Red.
     *     pixels[i] = 255;
     *     // Green.
     *     pixels[i + 1] = 0;
     *     // Blue.
     *     pixels[i + 2] = 0;
     *     // Alpha.
     *     pixels[i + 3] = 255;
     *   }
     *
     *   // Update the canvas.
     *   updatePixels();
     *
     *   describe('A red rectangle drawn above a gray rectangle.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.Color object.
     *   let pink = color(255, 102, 204);
     *
     *   // Load the pixels array.
     *   loadPixels();
     *
     *   // Get the pixel density.
     *   let d = pixelDensity();
     *
     *   // Calculate the halfway index in the pixels array.
     *   let halfImage = 4 * (d * width) * (d * height / 2);
     *
     *   // Make the top half of the canvas red.
     *   for (let i = 0; i < halfImage; i += 4) {
     *     pixels[i] = red(pink);
     *     pixels[i + 1] = green(pink);
     *     pixels[i + 2] = blue(pink);
     *     pixels[i + 3] = alpha(pink);
     *   }
     *
     *   // Update the canvas.
     *   updatePixels();
     *
     *   describe('A pink rectangle drawn above a gray rectangle.');
     * }
     */
  }

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

  class MatrixInterface {
    // Private field to store the matrix
    #matrix = null;
    constructor(...args) {
      if (this.constructor === MatrixInterface) {
        throw new Error("Class is of abstract type and can't be instantiated");
      }
      // TODO: don't check this at runtime but still at compile time
      /*const methods = [
        'add',
        'setElement',
        'reset',
        'set',
        'get',
        'copy',
        'clone',
        'diagonal',
        'row',
        'column',
        'transpose',
        'mult',
        'multiplyVec',
        'invert',
        'createSubMatrix3x3',
        'inverseTranspose4x4',
        'apply',
        'scale',
        'rotate4x4',
        'translate',
        'rotateX',
        'rotateY',
        'rotateZ',
        'perspective',
        'ortho',
        'multiplyVec4',
        'multiplyPoint',
        'multiplyAndNormalizePoint',
        'multiplyDirection',
        'multiplyVec3'
      ];

      methods.forEach(method => {
        if (this[method] === undefined) {
          throw new Error(`${method}() method must be implemented`);
        }
      });*/
    }
  }

  /**
   * @module Math
   */


  const isPerfectSquare = arr => {
    const sqDimention = Math.sqrt(arr.length);
    if (sqDimention % 1 !== 0) {
      throw new Error('Array length must be a perfect square.');
    }
    return true;
  };

  let GLMAT_ARRAY_TYPE = Array;
  let isMatrixArray = x => Array.isArray(x);
  if (typeof Float32Array !== 'undefined') {
    GLMAT_ARRAY_TYPE = Float32Array;
    isMatrixArray = x => Array.isArray(x) || x instanceof Float32Array;
  }

  class Matrix extends MatrixInterface {
    matrix;
    #sqDimention;

    constructor(...args) {
      super(...args);
      // This is default behavior when object
      // instantiated using createMatrix()
      if (isMatrixArray(args[0]) && isPerfectSquare(args[0])) {
        const sqDimention = Math.sqrt(args[0].length);
        this.#sqDimention = sqDimention;
        this.matrix = GLMAT_ARRAY_TYPE.from(args[0]);
      } else if (typeof args[0] === 'number') {
        this.#sqDimention = Number(args[0]);
        this.matrix = this.#createIdentityMatrix(args[0]);
      }
      return this;
    }

    /**
     * Returns the 3x3 matrix if the dimensions are 3x3, otherwise returns `undefined`.
     *
     * This method returns the matrix if its dimensions are 3x3.
     * If the matrix is not 3x3, it returns `undefined`.
     *
     * @returns {Array|undefined} The 3x3 matrix or `undefined` if the matrix is not 3x3.
     */
    get mat3() {
      if (this.#sqDimention === 3) {
        return this.matrix;
      } else {
        return undefined;
      }
    }

    /**
     * Returns the 4x4 matrix if the dimensions are 4x4, otherwise returns `undefined`.
     *
     * This method returns the matrix if its dimensions are 4x4.
     * If the matrix is not 4x4, it returns `undefined`.
     *
     * @returns {Array|undefined} The 4x4 matrix or `undefined` if the matrix is not 4x4.
     */
    get mat4() {
      if (this.#sqDimention === 4) {
        return this.matrix;
      } else {
        return undefined;
      }
    }

    /**
     * Adds the corresponding elements of the given matrix to this matrix, if the dimentions are the same.
     *
     * @param {Matrix} matrix - The matrix to add to this matrix. It must have the same dimensions as this matrix.
     * @returns {Matrix} The resulting matrix after addition.
     * @throws {Error} If the matrices do not have the same dimensions.
     *
     * @example
     * // META:norender
     * const matrix1 = new p5.Matrix([1, 2, 3]);
     * const matrix2 = new p5.Matrix([4, 5, 6]);
     * matrix1.add(matrix2); // matrix1 is now [5, 7, 9]
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix1 = new p5.Matrix([1, 2, 3, 4]);
     *   const matrix2 = new p5.Matrix([5, 6, 7, 8]);
     *   matrix1.add(matrix2);
     *   console.log(matrix1.matrix); // Output: [6, 8, 10, 12]
     * }
     */
    add(matrix) {
      if (this.matrix.length !== matrix.matrix.length) {
        throw new Error('Matrices must be of the same dimension to add.');
      }
      for (let i = 0; i < this.matrix.length; i++) {
        this.matrix[i] += matrix.matrix[i];
      }
      return this;
    }

    /**
     * Sets the value of a specific element in the matrix in column-major order.
     *
     * A matrix is stored in column-major order, meaning elements are arranged column by column.
     * This function allows you to update or change the value of a specific element
     * in the matrix by specifying its index in the column-major order and the new value.
     *
     * Parameters:
     * - `index` (number): The position in the matrix where the value should be set.
     *   Indices start from 0 and follow column-major order.
     * - `value` (any): The new value you want to assign to the specified element.
     *
     * Example:
     * If you have the following 3x3 matrix stored in column-major order:
     * ```
     * [
     *   1, 4, 7,  // Column 1
     *   2, 5, 8,  // Column 2
     *   3, 6, 9   // Column 3
     * ]
     * ```
     * Calling `setElement(4, 10)` will update the element at index 4
     * (which corresponds to row 2, column 2 in row-major order) to `10`.
     * The updated matrix will look like this:
     * ```
     * [
     *   1, 4, 7,
     *   2, 10, 8,
     *   3, 6, 9
     * ]
     * ```
     *
     * This function is useful for modifying specific parts of the matrix without
     * having to recreate the entire structure.
     *
     * @param {Number} index - The position in the matrix where the value should be set.
     *                         Must be a non-negative integer less than the length of the matrix.
     * @param {Number} value - The new value to be assigned to the specified position in the matrix.
     * @returns {Matrix} The current instance of the Matrix, allowing for method chaining.
     *
     * @example
     * // META:norender
     * // Assuming matrix is an instance of Matrix with initial values [1, 2, 3, 4] matrix.setElement(2, 99);
     * // Now the matrix values are [1, 2, 99, 4]
     * function setup() {
     *   const matrix = new p5.Matrix([1, 2, 3, 4]);
     *   matrix.setElement(2, 99);
     *   console.log(matrix.matrix); // Output: [1, 2, 99, 4]
     * }
     */
    setElement(index, value) {
      if (index >= 0 && index < this.matrix.length) {
        this.matrix[index] = value;
      }
      return this;
    }

    /**
     * Resets the current matrix to an identity matrix.
     *
     * This method replaces the current matrix with an identity matrix of the same dimensions.
     * An identity matrix is a square matrix with ones on the main diagonal and zeros elsewhere.
     * This is useful for resetting transformations or starting fresh with a clean matrix.
     *
     * @returns {Matrix} The current instance of the Matrix class, allowing for method chaining.
     *
     * @example
     * // META:norender
     * // Resetting a 4x4 matrix to an identity matrix
     * const matrix = new p5.Matrix(4);
     * matrix.scale(2, 2, 2); // Apply some transformations
     * console.log(matrix.matrix); // Output: Transformed matrix
     * matrix.reset(); // Reset to identity matrix
     * console.log(matrix.matrix); // Output: Identity matrix
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix = new p5.Matrix(4);
     *   matrix.scale(2, 2, 2); // Apply scaling transformation
     *   console.log("Before reset:", matrix.matrix);
     *   matrix.reset(); // Reset to identity matrix
     *   console.log("After reset:", matrix.matrix);
     * }
     */
    reset() {
      this.matrix = this.#createIdentityMatrix(this.#sqDimention);
      return this;
    }

    /**
     * Replace the entire contents of a NxN matrix.
     *
     * This method allows you to replace the values of the current matrix with
     * those from another matrix, an array, or individual arguments. The input
     * can be a `Matrix` instance, an array of numbers, or individual numbers
     * that match the dimensions of the current matrix. The values are copied
     * without referencing the source object, ensuring that the original input
     * remains unchanged.
     *
     * If the input dimensions do not match the current matrix, an error will
     * be thrown to ensure consistency.
     *
     * @param {Matrix|Float32Array|Number[]} [inMatrix] - The input matrix, array,
     * or individual numbers to replace the current matrix values.
     * @returns {Matrix} The current instance of the Matrix class, allowing for
     * method chaining.
     *
     * @example
     * // META:norender
     * // Replacing the contents of a matrix with another matrix
     * const matrix1 = new p5.Matrix([1, 2, 3, 4]);
     * const matrix2 = new p5.Matrix([5, 6, 7, 8]);
     * matrix1.set(matrix2);
     * console.log(matrix1.matrix); // Output: [5, 6, 7, 8]
     *
     * // Replacing the contents of a matrix with an array
     * const matrix = new p5.Matrix([1, 2, 3, 4]);
     * matrix.set([9, 10, 11, 12]);
     * console.log(matrix.matrix); // Output: [9, 10, 11, 12]
     *
     * // Replacing the contents of a matrix with individual numbers
     * const matrix = new p5.Matrix(4); // Creates a 4x4 identity matrix
     * matrix.set(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
     * console.log(matrix.matrix); // Output: [1, 2, 3, ..., 16]
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix = new p5.Matrix([1, 2, 3, 4]);
     *   console.log("Before set:", matrix.matrix);
     *   matrix.set([5, 6, 7, 8]);
     *   console.log("After set:", matrix.matrix); // Output: [5, 6, 7, 8]
     * }
     */
    set(inMatrix) {
      let refArray = GLMAT_ARRAY_TYPE.from([...arguments]);
      if (inMatrix instanceof Matrix) {
        refArray = GLMAT_ARRAY_TYPE.from(inMatrix.matrix);
      } else if (isMatrixArray(inMatrix)) {
        refArray = GLMAT_ARRAY_TYPE.from(inMatrix);
      }
      if (refArray.length !== this.matrix.length) {
        p5._friendlyError(
          `Expected same dimensions values but received different ${refArray.length}.`,
          'p5.Matrix.set'
        );
        return this;
      }
      this.matrix = refArray;
      return this;
    }

    /**
     * Gets a copy of the matrix, returns a p5.Matrix object.
     *
     * This method creates a new instance of the `Matrix` class and copies the
     * current matrix values into it. The returned matrix is independent of the
     * original, meaning changes to the copy will not affect the original matrix.
     *
     * This is useful when you need to preserve the current state of a matrix
     * while performing operations on a duplicate.
     *
     * @return {p5.Matrix} A new instance of the `Matrix` class containing the
     *                     same values as the original matrix.
     *
     * @example
     * // META:norender
     * function setup() {
     *   const originalMatrix = new p5.Matrix([1, 2, 3, 4]);
     *   const copiedMatrix = originalMatrix.get();
     *   console.log("Original Matrix:", originalMatrix.matrix); // Output: [1, 2, 3, 4]
     *   console.log("Copied Matrix:", copiedMatrix.matrix); // Output: [1, 2, 3, 4]
     *
     *   // Modify the copied matrix
     *   copiedMatrix.setElement(2, 99);
     *   console.log("Modified Copied Matrix:", copiedMatrix.matrix); // Output: [1, 2, 99, 4]
     *   console.log("Original Matrix remains unchanged:", originalMatrix.matrix); // Output: [1, 2, 3, 4]
     * }
     */
    get() {
      return new Matrix(this.matrix); // TODO: Pass p5
    }

    /**
     * Return a copy of this matrix.
     * If this matrix is 4x4, a 4x4 matrix with exactly the same entries will be
     * generated. The same is true if this matrix is 3x3 or any NxN matrix.
     *
     * This method is useful when you need to preserve the current state of a matrix
     * while performing operations on a duplicate. The returned matrix is independent
     * of the original, meaning changes to the copy will not affect the original matrix.
     *
     * @return {p5.Matrix}   The result matrix.
     *
     * @example
     * // META:norender
     * function setup() {
     *   const originalMatrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     *   const copiedMatrix = originalMatrix.copy();
     *   console.log("Original Matrix:", originalMatrix.matrix);
     *   console.log("Copied Matrix:", copiedMatrix.matrix);
     *
     *   // Modify the copied matrix
     *   copiedMatrix.setElement(4, 99);
     *   console.log("Modified Copied Matrix:", copiedMatrix.matrix);
     *   console.log("Original Matrix remains unchanged:", originalMatrix.matrix);
     * }
     */
    copy() {
      return new Matrix(this.matrix);
    }

    /**
     * Creates a copy of the current matrix instance.
     * This method is useful when you need a duplicate of the matrix
     * without modifying the original one.
     *
     * @returns {Matrix} A new matrix instance that is a copy of the current matrix.
     *
     * @example
     * // META:norender
     * function setup() {
     *   const originalMatrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     *   const clonedMatrix = originalMatrix.clone();
     *   console.log("Original Matrix:", originalMatrix.matrix);
     *   console.log("Cloned Matrix:", clonedMatrix.matrix);
     *
     *   // Modify the cloned matrix
     *   clonedMatrix.setElement(4, 99);
     *   console.log("Modified Cloned Matrix:", clonedMatrix.matrix);
     *   console.log("Original Matrix remains unchanged:", originalMatrix.matrix);
     * }
     */
    clone() {
      return this.copy();
    }

    /**
     * Returns the diagonal elements of the matrix in the form of an array.
     * A NxN matrix will return an array of length N.
     *
     * This method extracts the diagonal elements of the matrix, which are the
     * elements where the row index equals the column index. For example, in a
     * 3x3 matrix:
     * ```
     * [
     *   1, 2, 3,
     *   4, 5, 6,
     *   7, 8, 9
     * ]
     * ```
     * The diagonal elements are [1, 5, 9].
     *
     * This is useful for operations that require the main diagonal of a matrix,
     * such as calculating the trace of a matrix or verifying if a matrix is diagonal.
     *
     * @return {Number[]} An array obtained by arranging the diagonal elements
     *                    of the matrix in ascending order of index.
     *
     * @example
     * // META:norender
     * // Extracting the diagonal elements of a matrix
     * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     * const diagonal = matrix.diagonal(); // [1, 5, 9]
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     *   const diagonal = matrix.diagonal();
     *   console.log("Diagonal elements:", diagonal); // Output: [1, 5, 9]
     * }
     */
    diagonal() {
      const diagonal = [];
      for (let i = 0; i < this.#sqDimention; i++) {
        diagonal.push(this.matrix[i * (this.#sqDimention + 1)]);
      }
      return diagonal;
    }

    /**
     * This function is only for 3x3 matrices A function that returns a row vector of a NxN matrix.
     *
     * This method extracts a specific row from the matrix and returns it as a `p5.Vector`.
     * The row is determined by the `columnIndex` parameter, which specifies the column
     * index of the matrix. This is useful for operations that require working with
     * individual rows of a matrix, such as row transformations or dot products.
     *
     * @param {Number} columnIndex - The index of the column to extract as a row vector.
     *                               Must be a non-negative integer less than the matrix dimension.
     * @return {p5.Vector} A `p5.Vector` representing the extracted row of the matrix.
     *
     * @example
     * // META:norender
     * // Extracting a row vector from a 3x3 matrix
     * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     * const rowVector = matrix.row(1); // Returns a vector [2, 5, 8]
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     *   const rowVector = matrix.row(1); // Extract the second row (index 1)
     *   console.log("Row Vector:", rowVector.toString()); // Output: Row Vector: [2, 5, 8]
     * }
     */
    row(columnIndex) {
      const columnVector = [];
      for (let i = 0; i < this.#sqDimention; i++) {
        columnVector.push(this.matrix[i * this.#sqDimention + columnIndex]);
      }
      return new Vector(...columnVector);
    }

    /**
     * A function that returns a column vector of a NxN matrix.
     *
     * This method extracts a specific column from the matrix and returns it as a `p5.Vector`.
     * The column is determined by the `rowIndex` parameter, which specifies the row index
     * of the matrix. This is useful for operations that require working with individual
     * columns of a matrix, such as column transformations or dot products.
     *
     * @param {Number} rowIndex - The index of the row to extract as a column vector.
     *                             Must be a non-negative integer less than the matrix dimension.
     * @return {p5.Vector} A `p5.Vector` representing the extracted column of the matrix.
     *
     * @example
     * // META:norender
     * // Extracting a column vector from a 3x3 matrix
     * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     * const columnVector = matrix.column(1); // Returns a vector [4, 5, 6]
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     *   const columnVector = matrix.column(1); // Extract the second column (index 1)
     *   console.log("Column Vector:", columnVector.toString()); // Output: Column Vector: [4, 5, 6]
     * }
     */
    column(rowIndex) {
      const rowVector = [];
      for (let i = 0; i < this.#sqDimention; i++) {
        rowVector.push(this.matrix[rowIndex * this.#sqDimention + i]);
      }
      return new Vector(...rowVector);
    }

    /**
     * Transposes the given matrix `a` based on the square dimension of the matrix.
     *
     * This method rearranges the elements of the matrix such that the rows become columns
     * and the columns become rows. It handles matrices of different dimensions (4x4, 3x3, NxN)
     * by delegating to specific transpose methods for each case.
     *
     * If no argument is provided, the method transposes the current matrix instance.
     * If an argument is provided, it transposes the given matrix `a` and updates the current matrix.
     *
     * @param {Array} [a] - The matrix to be transposed. It should be a 2D array where each sub-array represents a row.
     *                      If omitted, the current matrix instance is transposed.
     * @returns {Matrix} - The current instance of the Matrix class, allowing for method chaining.
     *
     * @example
     * // META:norender
     * // Transposing a 3x3 matrix
     * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     * matrix.transpose();
     * console.log(matrix.matrix); // Output: [1, 4, 7, 2, 5, 8, 3, 6, 9]
     *
     * // Transposing a 4x4 matrix
     * const matrix4x4 = new p5.Matrix(4);
     * matrix4x4.transpose();
     * console.log(matrix4x4.matrix); // Output: Transposed 4x4 identity matrix
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     *   console.log("Before transpose:", matrix.matrix);
     *   matrix.transpose();
     *   console.log("After transpose:", matrix.matrix); // Output: [1, 4, 7, 2, 5, 8, 3, 6, 9]
     * }
     */
    transpose(a) {
      if (this.#sqDimention === 4) {
        return this.#transpose4x4(a);
      } else if (this.#sqDimention === 3) {
        return this.#transpose3x3(a);
      } else {
        return this.#transposeNxN(a);
      }
    }

    /**
     * Multiplies the current matrix with another matrix or matrix-like array.
     *
     * This method supports several types of input:
     * - Another Matrix instance
     * - A matrix-like array (must be a perfect square, e.g., 4x4 or 3x3)
     * - Multiple arguments that form a perfect square matrix
     *
     * If the input is the same as the current matrix, a copy is made to avoid modifying the original matrix.
     *
     * The method determines the appropriate multiplication strategy based on the dimensions of the current matrix
     * and the input matrix. It supports 3x3, 4x4, and NxN matrices.
     *
     * @param {Matrix|Array|...number} multMatrix - The matrix or matrix-like array to multiply with.
     * @returns {Matrix|undefined} The resulting matrix after multiplication, or undefined if the input is invalid.
     * @chainable
     *
     * @example
     * // META:norender
     * // Multiplying two 3x3 matrices
     * const matrix1 = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     * const matrix2 = new p5.Matrix([9, 8, 7, 6, 5, 4, 3, 2, 1]);
     * matrix1.mult(matrix2);
     * console.log(matrix1.matrix); // Output: [30, 24, 18, 84, 69, 54, 138, 114, 90]
     *
     * // Multiplying a 4x4 matrix with another 4x4 matrix
     * const matrix4x4_1 = new p5.Matrix(4); // Identity matrix
     * const matrix4x4_2 = new p5.Matrix([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 3, 1]);
     * matrix4x4_1.mult(matrix4x4_2);
     * console.log(matrix4x4_1.matrix); // Output: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 3, 1]
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix1 = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     *   const matrix2 = new p5.Matrix([9, 8, 7, 6, 5, 4, 3, 2, 1]);
     *   console.log("Before multiplication:", matrix1.matrix);
     *   matrix1.mult(matrix2);
     *   console.log("After multiplication:", matrix1.matrix); // Output: [30, 24, 18, 84, 69, 54, 138, 114, 90]
     * }
     */
    mult(multMatrix) {
      let _src;
      if (multMatrix === this || multMatrix === this.matrix) {
        _src = this.copy().matrix; // only need to allocate in this rare case
      } else if (multMatrix instanceof Matrix) {
        _src = multMatrix.matrix;
      } else if (isMatrixArray(multMatrix) && isPerfectSquare(multMatrix)) {
        _src = multMatrix;
      } else if (isPerfectSquare(Array.from(arguments))) {
        _src = Array.from(arguments);
      } else ;
      if (this.#sqDimention === 4 && _src.length === 16) {
        return this.#mult4x4(_src);
      } else if (this.#sqDimention === 3 && _src.length === 9) {
        return this.#mult3x3(_src);
      } else {
        return this.#multNxN(_src);
      }
    }

    /**
     * Takes a vector and returns the vector resulting from multiplying to that vector by this matrix from left. This function is only for 3x3 matrices.
     *
     * This method applies the current 3x3 matrix to a given vector, effectively
     * transforming the vector using the matrix. The resulting vector is returned
     * as a new vector or stored in the provided target vector.
     *
     * @param {p5.Vector} multVector - The vector to which this matrix applies.
     * @param {p5.Vector} [target] - The vector to receive the result. If not provided,
     *                               a copy of the input vector will be created and returned.
     * @return {p5.Vector} - The transformed vector after applying the matrix.
     *
     * @example
     * // META:norender
     * // Multiplying a 3x3 matrix with a vector
     * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     * const vector = new p5.Vector(1, 2, 3);
     * const result = matrix.multiplyVec(vector);
     * console.log(result.toString()); // Output: Transformed vector
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     *   const vector = new p5.Vector(1, 2, 3);
     *   const result = matrix.multiplyVec(vector);
     *   console.log("Original Vector:", vector.toString()); // Output : Original Vector: [1, 2, 3]
     *   console.log("Transformed Vector:", result.toString()); // Output : Transformed Vector: [30, 36, 42]
     * }
     */
    multiplyVec(multVector, target) {
      if (target === undefined) {
        target = multVector.copy();
      }
      for (let i = 0; i < this.#sqDimention; i++) {
        target.values[i] = this.row(i).dot(multVector);
      }
      return target;
    }

    /**
     * Inverts a given matrix.
     *
     * This method inverts a matrix based on its dimensions. Currently, it supports
     * 3x3 and 4x4 matrices. If the matrix dimension is greater than 4, an error is thrown.
     *
     * For 4x4 matrices, it uses a specialized algorithm to compute the inverse.
     * For 3x3 matrices, it uses a different algorithm optimized for smaller matrices.
     *
     * If the matrix is singular (non-invertible), the method will return `null`.
     *
     * @param {Array} a - The matrix to be inverted. It should be a 2D array representing the matrix.
     * @returns {Array|null} - The inverted matrix, or `null` if the matrix is singular.
     * @throws {Error} - Throws an error if the matrix dimension is greater than 4.
     *
     * @example
     * // META:norender
     * // Inverting a 3x3 matrix
     * const matrix = new p5.Matrix([1, 2, 3, 0, 1, 4, 5, 6, 0]);
     * const invertedMatrix = matrix.invert();
     * console.log(invertedMatrix.matrix); // Output: Inverted 3x3 matrix
     *
     * // Inverting a 4x4 matrix
     * const matrix4x4 = new p5.Matrix(4); // Identity matrix
     * matrix4x4.scale(2, 2, 2);
     * const invertedMatrix4x4 = matrix4x4.invert();
     * console.log(invertedMatrix4x4.matrix); // Output: Inverted 4x4 matrix
     *
     * @example
     * // META:norender
     * function setup() {
     *
     *   const matrix = new p5.Matrix([1, 2, 3, 0, 1, 4, 5, 6, 0]);
     *   console.log("Original Matrix:", matrix.matrix);
     *   const invertedMatrix = matrix.invert();
     *   if (invertedMatrix) {
     *     console.log("Inverted Matrix:", invertedMatrix.matrix);
     *   } else {
     *     console.log("Matrix is singular and cannot be inverted.");
     *   }
     * }
     */
    invert(a) {
      if (this.#sqDimention === 4) {
        return this.#invert4x4(a);
      } else if (this.#sqDimention === 3) {
        return this.#invert3x3(a);
      } else {
        throw new Error(
          'Invert is not implemented for N>4 at the moment, we are working on it'
        );
      }
    }

    /**
     * Creates a 3x3 matrix whose entries are the top left 3x3 part and returns it. This function is only for 4x4 matrices.
     *
     * This method extracts the top-left 3x3 portion of a 4x4 matrix and creates a new
     * 3x3 matrix from it. This is particularly useful in 3D graphics for operations
     * that require only the rotational or scaling components of a transformation matrix.
     *
     * If the current matrix is not 4x4, an error is thrown to ensure the method is used
     * correctly. The resulting 3x3 matrix is independent of the original matrix, meaning
     * changes to the new matrix will not affect the original.
     *
     * @return {p5.Matrix} A new 3x3 matrix containing the top-left portion of the original 4x4 matrix.
     * @throws {Error} If the current matrix is not 4x4.
     *
     * @example
     * // META:norender
     * // Extracting a 3x3 submatrix from a 4x4 matrix
     * const matrix4x4 = new p5.Matrix(4); // Creates a 4x4 identity matrix
     * matrix4x4.scale(2, 2, 2); // Apply scaling transformation
     * const subMatrix3x3 = matrix4x4.createSubMatrix3x3();
     * console.log("Original 4x4 Matrix:", matrix4x4.matrix);
     * console.log("Extracted 3x3 Submatrix:", subMatrix3x3.matrix);
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix4x4 = new p5.Matrix(4); // Creates a 4x4 identity matrix
     *   matrix4x4.scale(2, 2, 2); // Apply scaling transformation
     *   console.log("Original 4x4 Matrix:", matrix4x4.matrix);
     *
     *   const subMatrix3x3 = matrix4x4.createSubMatrix3x3();
     *   console.log("Extracted 3x3 Submatrix:", subMatrix3x3.matrix);
     * }
     */
    createSubMatrix3x3() {
      if (this.#sqDimention === 4) {
        const result = new Matrix(3);
        result.mat3[0] = this.matrix[0];
        result.mat3[1] = this.matrix[1];
        result.mat3[2] = this.matrix[2];
        result.mat3[3] = this.matrix[4];
        result.mat3[4] = this.matrix[5];
        result.mat3[5] = this.matrix[6];
        result.mat3[6] = this.matrix[8];
        result.mat3[7] = this.matrix[9];
        result.mat3[8] = this.matrix[10];
        return result;
      } else {
        throw new Error('Matrix dimension must be 4 to create a 3x3 submatrix.');
      }
    }

    /**
     * Converts a 4×4 matrix to its 3×3 inverse transpose transform.
     * This is commonly used in MVMatrix to NMatrix conversions, particularly
     * in 3D graphics for transforming normal vectors.
     *
     * This method extracts the top-left 3×3 portion of a 4×4 matrix, inverts it,
     * and then transposes the result. If the matrix is singular (non-invertible),
     * the resulting matrix will be zeroed out.
     *
     * @param  {p5.Matrix} mat4 - The 4×4 matrix to be converted.
     * @returns {Matrix} The current instance of the Matrix class, allowing for method chaining.
     * @throws {Error} If the current matrix is not 3×3.
     *
     * @example
     * // META:norender
     * // Converting a 4×4 matrix to its 3×3 inverse transpose
     * const mat4 = new p5.Matrix(4); // Create a 4×4 identity matrix
     * mat4.scale(2, 2, 2); // Apply scaling transformation
     * const mat3 = new p5.Matrix(3); // Create a 3×3 matrix
     * mat3.inverseTranspose4x4(mat4);
     * console.log("Converted 3×3 Matrix:", mat3.matrix);
     *
     * @example
     * // META:norender
     * function setup() {
     *   const mat4 = new p5.Matrix(4); // Create a 4×4 identity matrix
     *   mat4.scale(2, 2, 2); // Apply scaling transformation
     *   console.log("Original 4×4 Matrix:", mat4.matrix);
     *
     *   const mat3 = new p5.Matrix(3); // Create a 3×3 matrix
     *   mat3.inverseTranspose4x4(mat4);
     *   console.log("Converted 3×3 Matrix:", mat3.matrix);
     * }
     */
    inverseTranspose4x4({ mat4 }) {
      if (this.#sqDimention !== 3) {
        throw new Error('This function only works with 3×3 matrices.');
      } else {
        // Convert mat4 -> mat3 by extracting the top-left 3×3 portion
        this.matrix[0] = mat4[0];
        this.matrix[1] = mat4[1];
        this.matrix[2] = mat4[2];
        this.matrix[3] = mat4[4];
        this.matrix[4] = mat4[5];
        this.matrix[5] = mat4[6];
        this.matrix[6] = mat4[8];
        this.matrix[7] = mat4[9];
        this.matrix[8] = mat4[10];
      }

      const inverse = this.invert();
      // Check if inversion succeeded
      if (inverse) {
        inverse.transpose(this.matrix);
      } else {
        // In case of singularity, zero out the matrix
        for (let i = 0; i < 9; i++) {
          this.matrix[i] = 0;
        }
      }
      return this;
    }

    /**
     * Applies a transformation matrix to the current matrix.
     *
     * This method multiplies the current matrix by another matrix, which can be provided
     * in several forms: another Matrix instance, an array representing a matrix, or as
     * individual arguments representing the elements of a 4x4 matrix.
     *
     * This operation is useful for combining transformations such as translation, rotation,
     * scaling, and perspective projection into a single matrix. By applying a transformation
     * matrix, you can modify the current matrix to represent a new transformation.
     *
     * @param {Matrix|Array|number} multMatrix - The matrix to multiply with. This can be:
     *   - An instance of the Matrix class.
     *   - An array of 16 numbers representing a 4x4 matrix.
     *   - 16 individual numbers representing the elements of a 4x4 matrix.
     * @returns {Matrix} The current matrix after applying the transformation.
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Assuming `matrix` is an instance of Matrix
     *   const anotherMatrix = new p5.Matrix(4);
     *   const anotherMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
     *   matrix.apply(anotherMatrix);
     *
     *   // Applying a transformation using an array
     *   const matrixArray = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
     *   matrix.apply(matrixArray);
     *
     *   // Applying a transformation using individual arguments
     *   matrix.apply(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
     *
     *
     *   // Create a 4x4 identity matrix
     *   const matrix = new p5.Matrix(4);
     *   console.log("Original Matrix:", matrix.matrix);
     *
     *   // Create a scaling transformation matrix
     *   const scalingMatrix = new p5.Matrix([2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1]);
     *
     *   // Apply the scaling transformation
     *   matrix.apply(scalingMatrix);
     *   console.log("After Scaling Transformation:", matrix.matrix);
     *
     *   // Apply a translation transformation using an array
     *   const translationMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5, 5, 5, 1];
     *   matrix.apply(translationMatrix);
     *   console.log("After Translation Transformation:", matrix.matrix);
     * }
     */
    apply(multMatrix) {
      let _src;

      if (multMatrix === this || multMatrix === this.matrix) {
        _src = this.copy().matrix; // only need to allocate in this rare case
      } else if (multMatrix instanceof Matrix) {
        _src = multMatrix.matrix;
      } else if (isMatrixArray(multMatrix)) {
        _src = multMatrix;
      } else if (arguments.length === 16) {
        _src = arguments;
      } else {
        return; // nothing to do.
      }

      const mat4 = this.matrix;

      // each row is used for the multiplier
      const m0 = mat4[0];
      const m4 = mat4[4];
      const m8 = mat4[8];
      const m12 = mat4[12];
      mat4[0] = _src[0] * m0 + _src[1] * m4 + _src[2] * m8 + _src[3] * m12;
      mat4[4] = _src[4] * m0 + _src[5] * m4 + _src[6] * m8 + _src[7] * m12;
      mat4[8] = _src[8] * m0 + _src[9] * m4 + _src[10] * m8 + _src[11] * m12;
      mat4[12] = _src[12] * m0 + _src[13] * m4 + _src[14] * m8 + _src[15] * m12;

      const m1 = mat4[1];
      const m5 = mat4[5];
      const m9 = mat4[9];
      const m13 = mat4[13];
      mat4[1] = _src[0] * m1 + _src[1] * m5 + _src[2] * m9 + _src[3] * m13;
      mat4[5] = _src[4] * m1 + _src[5] * m5 + _src[6] * m9 + _src[7] * m13;
      mat4[9] = _src[8] * m1 + _src[9] * m5 + _src[10] * m9 + _src[11] * m13;
      mat4[13] = _src[12] * m1 + _src[13] * m5 + _src[14] * m9 + _src[15] * m13;

      const m2 = mat4[2];
      const m6 = mat4[6];
      const m10 = mat4[10];
      const m14 = mat4[14];
      mat4[2] = _src[0] * m2 + _src[1] * m6 + _src[2] * m10 + _src[3] * m14;
      mat4[6] = _src[4] * m2 + _src[5] * m6 + _src[6] * m10 + _src[7] * m14;
      mat4[10] = _src[8] * m2 + _src[9] * m6 + _src[10] * m10 + _src[11] * m14;
      mat4[14] = _src[12] * m2 + _src[13] * m6 + _src[14] * m10 + _src[15] * m14;

      const m3 = mat4[3];
      const m7 = mat4[7];
      const m11 = mat4[11];
      const m15 = mat4[15];
      mat4[3] = _src[0] * m3 + _src[1] * m7 + _src[2] * m11 + _src[3] * m15;
      mat4[7] = _src[4] * m3 + _src[5] * m7 + _src[6] * m11 + _src[7] * m15;
      mat4[11] = _src[8] * m3 + _src[9] * m7 + _src[10] * m11 + _src[11] * m15;
      mat4[15] = _src[12] * m3 + _src[13] * m7 + _src[14] * m11 + _src[15] * m15;

      return this;
    }

    /**
     * Scales a p5.Matrix by scalars or a vector.
     *
     * This method applies a scaling transformation to the current matrix.
     * Scaling is a transformation that enlarges or shrinks objects by a scale factor
     * along the x, y, and z axes. The scale factors can be provided as individual
     * numbers, an array, or a `p5.Vector`.
     *
     * If a `p5.Vector` or an array is provided, the x, y, and z components are extracted
     * from it. If the z component is not provided, it defaults to 1 (no scaling along the z-axis).
     *
     * @param {p5.Vector|Float32Array|Number[]} s - The vector or scalars to scale by.
     *                                              Can be a `p5.Vector`, an array, or individual numbers.
     * @returns {Matrix} The current instance of the Matrix class, allowing for method chaining.
     *
     * @example
     * // META:norender
     * // Scaling a matrix by individual scalars
     * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     * matrix.scale(2, 3, 4); // Scale by 2 along x, 3 along y, and 4 along z
     * console.log(matrix.matrix);
     *
     * // Scaling a matrix by a p5.Vector
     * const scaleVector = new p5.Vector(2, 3, 4);
     * matrix.scale(scaleVector);
     * console.log(matrix.matrix);
     *
     * // Scaling a matrix by an array
     * const scaleArray = [2, 3, 4];
     * matrix.scale(scaleArray);
     * console.log(matrix.matrix);
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     *   console.log("Original Matrix:", matrix.matrix);
     *
     *   // Scale the matrix by individual scalars
     *   matrix.scale(2, 3, 4);
     *   console.log("Scaled Matrix (2, 3, 4):", matrix.matrix);
     *
     *   // Scale the matrix by a p5.Vector
     *   const scaleVector = new p5.Vector(1.5, 2.5, 3.5);
     *   matrix.scale(scaleVector);
     *   console.log("Scaled Matrix (Vector):", matrix.matrix);
     *
     *   // Scale the matrix by an array
     *   const scaleArray = [0.5, 0.5, 0.5];
     *   matrix.scale(scaleArray);
     *   console.log("Scaled Matrix (Array):", matrix.matrix);
     * }
     */
    scale(x, y, z) {
      if (x instanceof Vector) {
        // x is a vector, extract the components from it.
        y = x.y;
        z = x.z;
        x = x.x; // must be last
      } else if (x instanceof Array) {
        // x is an array, extract the components from it.
        y = x[1];
        z = x[2];
        x = x[0]; // must be last
      }

      this.matrix[0] *= x;
      this.matrix[1] *= x;
      this.matrix[2] *= x;
      this.matrix[3] *= x;
      this.matrix[4] *= y;
      this.matrix[5] *= y;
      this.matrix[6] *= y;
      this.matrix[7] *= y;
      this.matrix[8] *= z;
      this.matrix[9] *= z;
      this.matrix[10] *= z;
      this.matrix[11] *= z;

      return this;
    }

    /**
     * Rotate the Matrix around a specified axis by a given angle.
     *
     * This method applies a rotation transformation to the matrix, modifying its orientation
     * in 3D space. The rotation is performed around the provided axis, which can be defined
     * as a `p5.Vector` or an array of numbers representing the x, y, and z components of the axis.
     * Rotate our Matrix around an axis by the given angle.
     * @param  {Number} a The angle of rotation in radians.
     *                    Angles in radians are a measure of rotation, where 2π radians
     *                    represent a full circle (360 degrees). For example:
     *                    - π/2 radians = 90 degrees (quarter turn)
     *                    - π radians = 180 degrees (half turn)
     *                    - 2π radians = 360 degrees (full turn)
     *                    Use `Math.PI` for π or `p5`'s `PI` constant if using p5.js.
     * @param  {p5.Vector|Number[]} axis The axis or axes to rotate around.
     *                                   This defines the direction of the rotation.
     *                                   - If using a `p5.Vector`, it should represent
     *                                     the x, y, and z components of the axis.
     *                                   - If using an array, it should be in the form
     *                                     [x, y, z], where x, y, and z are numbers.
     *                                   For example:
     *                                   - [1, 0, 0] rotates around the x-axis.
     *                                   - [0, 1, 0] rotates around the y-axis.
     *                                   - [0, 0, 1] rotates around the z-axis.   *
     * @chainable
     * inspired by Toji's gl-matrix lib, mat4 rotation
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     *   console.log("Original Matrix:", matrix.matrix.slice().toString()); // [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]
     *
     *   // Translate the matrix by a 3D vector
     *   matrix.rotate4x4(Math.PI, [1,0,0]);
     *   console.log("After rotation of PI degrees on vector [1,0,0]:", matrix.matrix.slice().toString()); // [1,0,0,0,0,-1,1.2246468525851679e-16,0,0,-1.2246468525851679e-16,-1,0,0,0,0,1]
     * }
     */
    rotate4x4(a, x, y, z) {
      if (x instanceof Vector) {
        // x is a vector, extract the components from it.
        y = x.y;
        z = x.z;
        x = x.x; //must be last
      } else if (x instanceof Array) {
        // x is an array, extract the components from it.
        y = x[1];
        z = x[2];
        x = x[0]; //must be last
      }

      const len = Math.sqrt(x * x + y * y + z * z);
      x *= 1 / len;
      y *= 1 / len;
      z *= 1 / len;

      const a00 = this.matrix[0];
      const a01 = this.matrix[1];
      const a02 = this.matrix[2];
      const a03 = this.matrix[3];
      const a10 = this.matrix[4];
      const a11 = this.matrix[5];
      const a12 = this.matrix[6];
      const a13 = this.matrix[7];
      const a20 = this.matrix[8];
      const a21 = this.matrix[9];
      const a22 = this.matrix[10];
      const a23 = this.matrix[11];

      //sin,cos, and tan of respective angle
      const sA = Math.sin(a);
      const cA = Math.cos(a);
      const tA = 1 - cA;
      // Construct the elements of the rotation matrix
      const b00 = x * x * tA + cA;
      const b01 = y * x * tA + z * sA;
      const b02 = z * x * tA - y * sA;
      const b10 = x * y * tA - z * sA;
      const b11 = y * y * tA + cA;
      const b12 = z * y * tA + x * sA;
      const b20 = x * z * tA + y * sA;
      const b21 = y * z * tA - x * sA;
      const b22 = z * z * tA + cA;

      // rotation-specific matrix multiplication
      this.matrix[0] = a00 * b00 + a10 * b01 + a20 * b02;
      this.matrix[1] = a01 * b00 + a11 * b01 + a21 * b02;
      this.matrix[2] = a02 * b00 + a12 * b01 + a22 * b02;
      this.matrix[3] = a03 * b00 + a13 * b01 + a23 * b02;
      this.matrix[4] = a00 * b10 + a10 * b11 + a20 * b12;
      this.matrix[5] = a01 * b10 + a11 * b11 + a21 * b12;
      this.matrix[6] = a02 * b10 + a12 * b11 + a22 * b12;
      this.matrix[7] = a03 * b10 + a13 * b11 + a23 * b12;
      this.matrix[8] = a00 * b20 + a10 * b21 + a20 * b22;
      this.matrix[9] = a01 * b20 + a11 * b21 + a21 * b22;
      this.matrix[10] = a02 * b20 + a12 * b21 + a22 * b22;
      this.matrix[11] = a03 * b20 + a13 * b21 + a23 * b22;

      return this;
    }

    /**
     * Translates the current matrix by a given vector.
     *
     * This method applies a translation transformation to the current matrix.
     * Translation moves the matrix by a specified amount along the x, y, and z axes.
     * The input vector can be a 2D or 3D vector. If the z-component is not provided,
     * it defaults to 0, meaning no translation along the z-axis.
     *
     * @param {Number[]} v - A vector representing the translation. It should be an array
     *                       with two or three elements: [x, y, z]. The z-component is optional.
     * @returns {Matrix} The current instance of the Matrix class, allowing for method chaining.
     *
     * @example
     * // META:norender
     * // Translating a matrix by a 3D vector
     * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     * matrix.translate([10, 20, 30]); // Translate by 10 units along x, 20 along y, and 30 along z
     * console.log(matrix.matrix);
     *
     * // Translating a matrix by a 2D vector
     * matrix.translate([5, 15]); // Translate by 5 units along x and 15 along y
     * console.log(matrix.matrix);
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     *   console.log("Original Matrix:", matrix.matrix.slice().toString()); // [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]
     *
     *   // Translate the matrix by a 3D vector
     *   matrix.translate([10, 20, 30]);
     *   console.log("After 3D Translation (10, 20, 30):", matrix.matrix.slice().toString()); // [1,0,0,0,0,1,0,0,0,0,1,0,10,20,30,1]
     *
     *   // Translate the matrix by a 2D vector
     *   matrix.translate([5, 15]);
     *   console.log("After 2D Translation (5, 15):", matrix.matrix.slice().toString()); // [1,0,0,0,0,1,0,0,0,0,1,0,15,35,30,1]
     * }
     */
    translate(v) {
      const x = v[0],
        y = v[1],
        z = v[2] || 0;
      this.matrix[12] +=
        this.matrix[0] * x + this.matrix[4] * y + this.matrix[8] * z;
      this.matrix[13] +=
        this.matrix[1] * x + this.matrix[5] * y + this.matrix[9] * z;
      this.matrix[14] +=
        this.matrix[2] * x + this.matrix[6] * y + this.matrix[10] * z;
      this.matrix[15] +=
        this.matrix[3] * x + this.matrix[7] * y + this.matrix[11] * z;
      return this;
    }

    /**
     * Rotates the matrix around the X-axis by a given angle.
     *
     * This method modifies the current matrix to apply a rotation transformation
     * around the X-axis. The rotation angle is specified in radians.
     *
     * Rotating around the X-axis means that the Y and Z coordinates of the matrix
     * are transformed while the X coordinates remain unchanged. This is commonly
     * used in 3D graphics to create animations or transformations along the X-axis.
     *
     * @param {Number} a - The angle in radians to rotate the matrix by.
     *
     * @example
     * // META:norender
     * // Rotating a matrix around the X-axis
     * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     * matrix.rotateX(Math.PI / 4); // Rotate 45 degrees around the X-axis
     * console.log(matrix.matrix);
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     *   console.log("Original Matrix:", matrix.matrix);
     *
     *   // Rotate the matrix 45 degrees (PI/4 radians) around the X-axis
     *   matrix.rotateX(Math.PI / 4);
     *   console.log("After Rotation (X-axis, 45 degrees):", matrix.matrix);
     * }
     */
    rotateX(a) {
      this.rotate4x4(a, 1, 0, 0);
    }

    /**
     * Rotates the matrix around the Y-axis by a given angle.
     *
     * This method modifies the current matrix to apply a rotation transformation
     * around the Y-axis. The rotation is performed in 3D space, and the angle
     * is specified in radians. Rotating around the Y-axis means that the X and Z
     * coordinates of the matrix are transformed while the Y coordinates remain
     * unchanged. This is commonly used in 3D graphics to create animations or
     * transformations along the Y-axis.
     *
     * @param {Number} a - The angle in radians to rotate the matrix by. Positive
     * values rotate the matrix counterclockwise, and negative values rotate it
     * clockwise.
     *
     * @example
     * // META:norender
     * // Rotating a matrix around the Y-axis
     * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     * matrix.rotateY(Math.PI / 4); // Rotate 45 degrees around the Y-axis
     * console.log(matrix.matrix);
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     *   console.log("Original Matrix:", matrix.matrix);
     *
     *   // Rotate the matrix 45 degrees (PI/4 radians) around the Y-axis
     *   matrix.rotateY(Math.PI / 4);
     *   console.log("After Rotation (Y-axis, 45 degrees):", matrix.matrix);
     * }
     */
    rotateY(a) {
      this.rotate4x4(a, 0, 1, 0);
    }

    /**
     * Rotates the matrix around the Z-axis by a given angle.
     *
     * This method modifies the current matrix to apply a rotation transformation
     * around the Z-axis. The rotation is performed in a 4x4 matrix context, which
     * is commonly used in 3D graphics to handle transformations. Rotating around
     * the Z-axis means that the X and Y coordinates of the matrix are transformed
     * while the Z coordinates remain unchanged.
     *
     * @param {Number} a - The angle in radians to rotate the matrix by. Positive
     * values rotate the matrix counterclockwise, and negative values rotate it
     * clockwise.
     *
     * @returns {Matrix} The current instance of the Matrix class, allowing for
     * method chaining.
     *
     * @example
     * // META:norender
     * // Rotating a matrix around the Z-axis
     * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     * matrix.rotateZ(Math.PI / 4); // Rotate 45 degrees around the Z-axis
     * console.log(matrix.matrix);
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     *   console.log("Original Matrix:", matrix.matrix);
     *
     *   // Rotate the matrix 45 degrees (PI/4 radians) around the Z-axis
     *   matrix.rotateZ(Math.PI / 4);
     *   console.log("After Rotation (Z-axis, 45 degrees):", matrix.matrix);
     * }
     */
    rotateZ(a) {
      this.rotate4x4(a, 0, 0, 1);
    }

    /**
     * Sets the perspective projection matrix.
     *
     * This method modifies the current matrix to represent a perspective projection.
     * Perspective projection is commonly used in 3D graphics to simulate the effect
     * of objects appearing smaller as they move further away from the camera.
     *
     * The perspective matrix is defined by the field of view (fovy), aspect ratio,
     * and the near and far clipping planes. The near and far clipping planes define
     * the range of depth that will be rendered, with anything outside this range
     * being clipped.
     *
     * @param {Number} fovy - The field of view in the y direction, in radians.
     * @param {Number} aspect - The aspect ratio of the viewport (width / height).
     * @param {Number} near - The distance to the near clipping plane. Must be greater than 0.
     * @param {Number} far - The distance to the far clipping plane. Must be greater than the near value.
     * @returns {Matrix} The current instance of the Matrix class, allowing for method chaining.
     *
     * @example
     * // META:norender
     * // Setting a perspective projection matrix
     * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     * matrix.perspective(Math.PI / 4, 1.5, 0.1, 100); // Set perspective projection
     * console.log(matrix.matrix);
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     *   console.log("Original Matrix:", matrix.matrix);
     *
     *   // Set a perspective projection with a 45-degree field of view,
     *   // an aspect ratio of 1.5, and near/far clipping planes at 0.1 and 100.
     *   matrix.perspective(Math.PI / 4, 1.5, 0.1, 100);
     *   console.log("Perspective Matrix:", matrix.matrix);
     * }
     */
    perspective(fovy, aspect, near, far) {
      const f = 1.0 / Math.tan(fovy / 2),
        nf = 1 / (near - far);

      this.matrix[0] = f / aspect;
      this.matrix[1] = 0;
      this.matrix[2] = 0;
      this.matrix[3] = 0;
      this.matrix[4] = 0;
      this.matrix[5] = f;
      this.matrix[6] = 0;
      this.matrix[7] = 0;
      this.matrix[8] = 0;
      this.matrix[9] = 0;
      this.matrix[10] = (far + near) * nf;
      this.matrix[11] = -1;
      this.matrix[12] = 0;
      this.matrix[13] = 0;
      this.matrix[14] = 2 * far * near * nf;
      this.matrix[15] = 0;

      return this;
    }

    /**
     * Sets this matrix to an orthographic projection matrix.
     *
     * An orthographic projection matrix is used to create a 2D rendering
     * of a 3D scene by projecting points onto a plane without perspective
     * distortion. This method modifies the current matrix to represent
     * the orthographic projection defined by the given parameters.
     *
     * @param {number} left - The coordinate for the left vertical clipping plane.
     * @param {number} right - The coordinate for the right vertical clipping plane.
     * @param {number} bottom - The coordinate for the bottom horizontal clipping plane.
     * @param {number} top - The coordinate for the top horizontal clipping plane.
     * @param {number} near - The distance to the near depth clipping plane. Must be positive.
     * @param {number} far - The distance to the far depth clipping plane. Must be positive.
     * @chainable
     * @returns {Matrix} The current matrix instance, updated with the orthographic projection.
     *
     * @example
     * // META:norender
     * // Example using p5.js to demonstrate orthographic projection
     * function setup() {
     *  let orthoMatrix = new p5.Matrix(4);
     *  console.log(orthoMatrix.matrix.toString()) // Output: 1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1
     *  orthoMatrix.ortho(-200, 200, -200, 200, 0.1, 1000);
     *  console.log(orthoMatrix.matrix.toString()) // Output: [24 0.004999999888241291,0,0,0,0,0.004999999888241291,0,0,0,0,-0.0020002000965178013,0,0,0,-1.0002000331878662,1]
     *  applyMatrix(
     *     orthoMatrix.mat4[0], orthoMatrix.mat4[1], orthoMatrix.mat4[2], orthoMatrix.mat4[3],
     *     orthoMatrix.mat4[4], orthoMatrix.mat4[5], orthoMatrix.mat4[6], orthoMatrix.mat4[7],
     *     orthoMatrix.mat4[8], orthoMatrix.mat4[9], orthoMatrix.mat4[10], orthoMatrix.mat4[11],
     *     orthoMatrix.mat4[12], orthoMatrix.mat4[13], orthoMatrix.mat4[14], orthoMatrix.mat4[15]
     *  );
     *  console.log(orthoMatrix.matrix.toString()) // Output: [31 0.004999999888241291,0,0,0,0,0.004999999888241291,0,0,0,0,-0.0020002000965178013,0,0,0,-1.0002000331878662,1]
     *   }
     */
    ortho(left, right, bottom, top, near, far) {
      const lr = 1 / (left - right),
        bt = 1 / (bottom - top),
        nf = 1 / (near - far);
      this.matrix[0] = -2 * lr;
      this.matrix[1] = 0;
      this.matrix[2] = 0;
      this.matrix[3] = 0;
      this.matrix[4] = 0;
      this.matrix[5] = -2 * bt;
      this.matrix[6] = 0;
      this.matrix[7] = 0;
      this.matrix[8] = 0;
      this.matrix[9] = 0;
      this.matrix[10] = 2 * nf;
      this.matrix[11] = 0;
      this.matrix[12] = (left + right) * lr;
      this.matrix[13] = (top + bottom) * bt;
      this.matrix[14] = (far + near) * nf;
      this.matrix[15] = 1;

      return this;
    }

    /**
     * Applies a matrix to a vector with x, y, z, w components and returns the result as an array.
     *
     * This method multiplies the current matrix by a 4D vector (x, y, z, w) and computes the resulting vector.
     * It is commonly used in 3D graphics for transformations such as translation, rotation, scaling, and perspective projection.
     *
     * The resulting vector is returned as an array of four numbers, representing the transformed x, y, z, and w components.
     *
     * @param {Number} x - The x component of the vector.
     * @param {Number} y - The y component of the vector.
     * @param {Number} z - The z component of the vector.
     * @param {Number} w - The w component of the vector.
     * @returns {Number[]} An array containing the transformed [x, y, z, w] components.
     *
     * @example
     * // META:norender
     * // Applying a matrix to a 4D vector
     * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     * const result = matrix.multiplyVec4(1, 2, 3, 1); // Transform the vector [1, 2, 3, 1]
     * console.log(result); // Output: [1, 2, 3, 1] (unchanged for identity matrix)
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     *   console.log("Original Matrix:", matrix.matrix);
     *
     *   // Apply the matrix to a 4D vector
     *   const result = matrix.multiplyVec4(1, 2, 3, 1);
     *   console.log("Transformed Vector:", result); // Output: [1, 2, 3, 1]
     *
     *   // Modify the matrix (e.g., apply a translation)
     *   matrix.translate([5, 5, 5]);
     *   console.log("Modified Matrix:", matrix.matrix);
     *
     *   // Apply the modified matrix to the same vector
     *   const transformedResult = matrix.multiplyVec4(1, 2, 3, 1);
     *   console.log("Transformed Vector after Translation:", transformedResult); // Output: [6, 7, 8, 1]
     * }
     */
    multiplyVec4(x, y, z, w) {
      const result = new Array(4);
      const m = this.matrix;

      result[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w;
      result[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w;
      result[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w;
      result[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w;

      return result;
    }

    /**
     * Applies a matrix to a vector. The fourth component is set to 1.
     * Returns a vector consisting of the first
     * through third components of the result.
     *
     * This method multiplies the current matrix by a 4D vector (x, y, z, 1),
     * effectively transforming the vector using the matrix. The resulting
     * vector is returned as a new `p5.Vector` instance.
     *
     * This is useful for applying transformations such as translation,
     * rotation, scaling, or perspective projection to a point in 3D space.
     *
     * @param {p5.Vector} vector - The input vector to transform. It should
     *                              have x, y, and z components.
     * @return {p5.Vector} A new `p5.Vector` instance representing the transformed point.
     *
     * @example
     * // META:norender
     * // Applying a matrix to a 3D point
     * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     * const point = new p5.Vector(1, 2, 3); // Define a 3D point
     * const transformedPoint = matrix.multiplyPoint(point);
     * console.log(transformedPoint.toString()); // Output: [1, 2, 3] (unchanged for identity matrix)
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     *   console.log("Original Matrix:", matrix.matrix);
     *
     *   // Define a 3D point
     *   const point = new p5.Vector(1, 2, 3);
     *   console.log("Original Point:", point.toString());
     *
     *   // Apply the matrix to the point
     *   const transformedPoint = matrix.multiplyPoint(point);
     *   console.log("Transformed Point:", transformedPoint.toString());
     *
     *   // Modify the matrix (e.g., apply a translation)
     *   matrix.translate([5, 5, 5]);
     *   console.log("Modified Matrix:", matrix.matrix);
     *
     *   // Apply the modified matrix to the same point
     *   const translatedPoint = matrix.multiplyPoint(point);
     *   console.log("Translated Point:", translatedPoint.toString()); // Output: [6, 7, 8]
     * }
     */
    multiplyPoint({ x, y, z }) {
      const array = this.multiplyVec4(x, y, z, 1);
      return new Vector(array[0], array[1], array[2]);
    }

    /**
     * Applies a matrix to a vector.
     * The fourth component is set to 1.
     * Returns the result of dividing the 1st to 3rd components
     * of the result by the 4th component as a vector.
     *
     * This method multiplies the current matrix by a 4D vector (x, y, z, 1),
     * effectively transforming the vector using the matrix. The resulting
     * vector is normalized by dividing its x, y, and z components by the w component.
     * This is useful for applying transformations such as perspective projection
     * to a point in 3D space.
     *
     * @param {p5.Vector} vector - The input vector to transform. It should
     *                              have x, y, and z components.
     * @return {p5.Vector} A new `p5.Vector` instance representing the transformed and normalized point.
     *
     * @example
     * // META:norender
     * // Applying a matrix to a 3D point and normalizing it
     * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     * const point = new p5.Vector(1, 2, 3); // Define a 3D point
     * const transformedPoint = matrix.multiplyAndNormalizePoint(point);
     * console.log(transformedPoint.toString()); // Output: [1, 2, 3] (unchanged for identity matrix)
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     *   console.log("Original Matrix:", matrix.matrix);
     *
     *   // Define a 3D point
     *   const point = new p5.Vector(1, 2, 3);
     *   console.log("Original Point:", point.toString());
     *
     *   // Apply the matrix to the point and normalize it
     *   const transformedPoint = matrix.multiplyAndNormalizePoint(point);
     *   console.log("Transformed and Normalized Point:", transformedPoint.toString());
     *
     *   // Modify the matrix (e.g., apply a perspective transformation)
     *   matrix.perspective(Math.PI / 4, 1.5, 0.1, 100);
     *   console.log("Modified Matrix (Perspective):", matrix.matrix);
     *
     *   // Apply the modified matrix to the same point
     *   const perspectivePoint = matrix.multiplyAndNormalizePoint(point);
     *   console.log("Point after Perspective Transformation:", perspectivePoint.toString());
     * }
     */
    multiplyAndNormalizePoint({ x, y, z }) {
      const array = this.multiplyVec4(x, y, z, 1);
      array[0] /= array[3];
      array[1] /= array[3];
      array[2] /= array[3];
      return new Vector(array[0], array[1], array[2]);
    }

    /**
     * Applies a matrix to a vector.
     * The fourth component is set to 0.
     * Returns a vector consisting of the first
     * through third components of the result.
     *
     * This method multiplies the current matrix by a 4D vector (x, y, z, 0),
     * effectively transforming the direction vector using the matrix. The resulting
     * vector is returned as a new `p5.Vector` instance. This is particularly useful
     * for transforming direction vectors (e.g., normals) without applying translation.
     *
     * @param {p5.Vector} vector - The input vector to transform. It should
     *                              have x, y, and z components.
     * @return {p5.Vector} A new `p5.Vector` instance representing the transformed direction.
     *
     * @example
     * // META:norender
     * // Applying a matrix to a direction vector
     * const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     * const direction = new p5.Vector(1, 0, 0); // Define a direction vector
     * const transformedDirection = matrix.multiplyDirection(direction);
     * console.log(transformedDirection.toString()); // Output: [1, 0, 0] (unchanged for identity matrix)
     *
     * @example
     * // META:norender
     * function setup() {
     *   const matrix = new p5.Matrix(4); // Create a 4x4 identity matrix
     *   console.log("Original Matrix:", matrix.matrix);
     *
     *   // Define a direction vector
     *   const direction = new p5.Vector(1, 0, 0);
     *   console.log("Original Direction:", direction.toString());
     *
     *   // Apply the matrix to the direction vector
     *   const transformedDirection = matrix.multiplyDirection(direction);
     *   console.log("Transformed Direction:", transformedDirection.toString());
     *
     *   // Modify the matrix (e.g., apply a rotation)
     *   matrix.rotateY(Math.PI / 4); // Rotate 45 degrees around the Y-axis
     *   console.log("Modified Matrix (Rotation):", matrix.matrix);
     *
     *   // Apply the modified matrix to the same direction vector
     *   const rotatedDirection = matrix.multiplyDirection(direction);
     *   console.log("Rotated Direction:", rotatedDirection.toString()); // Output: Rotated vector
     * }
     */
    multiplyDirection({ x, y, z }) {
      const array = this.multiplyVec4(x, y, z, 0);
      return new Vector(array[0], array[1], array[2]);
    }

    /**
     * Takes a vector and returns the vector resulting from multiplying. This function is only for 3x3 matrices.
     * that vector by this matrix from the left.
     *
     * This method applies the current 3x3 matrix to a given vector, effectively
     * transforming the vector using the matrix. The resulting vector is returned
     * as a new vector or stored in the provided target vector.
     *
     * This is useful for operations such as transforming points or directions
     * in 2D or 3D space using a 3x3 transformation matrix.
     *
     * @param {p5.Vector} multVector - The vector to which this matrix applies.
     * @param {p5.Vector} [target] - The vector to receive the result. If not provided,
     *                               a copy of the input vector will be created and returned.
     * @return {p5.Vector} - The transformed vector after applying the matrix.
     *
     * @example
     * // META:norender
     * // Multiplying a 3x3 matrix with a vector
     * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     * const vector = new p5.Vector(1, 2, 3);
     * const result = matrix.multiplyVec3(vector);
     * console.log(result.toString()); // Output: Transformed vector
     *
     * @example
     * // META:norender
     * function setup() {
     *   // Create a 3x3 matrix
     *   const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     *   console.log("Original Matrix:", matrix.matrix);
     *
     *   // Define a vector
     *   const vector = new p5.Vector(1, 2, 3);
     *   console.log("Original Vector:", vector.toString()); // Output: [1, 2, 3]
     *
     *   // Apply the matrix to the vector
     *   const transformedVector = matrix.multiplyVec3(vector);
     *   console.log("Transformed Vector:", transformedVector.toString()); // Output: [30, 36, 42]
     *
     *   // Modify the matrix (e.g., apply a scaling transformation)
     *   matrix.scale(2, 2, 2);
     *   console.log("Modified Matrix (Scaling):", matrix.matrix); // Output: [2, 4, 6, 8, 10, 12, 14, 16, 18]
     *
     *   // Apply the modified matrix to the same vector
     *   const scaledVector = matrix.multiplyVec3(vector);
     *   console.log("Scaled Vector:", scaledVector.toString()); // Output: [60, 72, 84]
     * }
     */
    multiplyVec3(multVector, target) {
      if (target === undefined) {
        target = multVector.copy();
      }
      target.x = this.row(0).dot(multVector);
      target.y = this.row(1).dot(multVector);
      target.z = this.row(2).dot(multVector);
      return target;
    }

    // ====================
    // PRIVATE
    /**
     * Creates identity matrix
     * This method updates the current matrix with the result of the multiplication.
     *
     * @private
     */
    #createIdentityMatrix(dimension) {
      // This it to prevent loops in the most common 3x3 and 4x4 cases
      // TODO: check performance if it actually helps
      if (dimension === 3)
        return new GLMAT_ARRAY_TYPE([1, 0, 0, 0, 1, 0, 0, 0, 1]);
      if (dimension === 4)
        return new GLMAT_ARRAY_TYPE([
          1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1
        ]);
      const identityMatrix = new GLMAT_ARRAY_TYPE(dimension * dimension).fill(0);
      for (let i = 0; i < dimension; i++) {
        identityMatrix[i * dimension + i] = 1;
      }
      return identityMatrix;
    }

    /**
     * Multiplies the current 4x4 matrix with another 4x4 matrix.
     * This method updates the current matrix with the result of the multiplication.
     *
     * @private
     * @param {number[]} _src - A 16-element array representing the 4x4 matrix to multiply with.
     *
     * @returns {this} The current instance with the updated matrix.
     *
     * @example
     * // META:norender
     * // Assuming `matrix` is an instance of the Matrix class
     * const srcMatrix = [
     *   1, 0, 0, 0,
     *   0, 1, 0, 0,
     *   0, 0, 1, 0,
     *   0, 0, 0, 1
     * ];
     * matrix.#mult4x4(srcMatrix);
     */
    #mult4x4(_src) {
      // each row is used for the multiplier
      let b0 = this.matrix[0],
        b1 = this.matrix[1],
        b2 = this.matrix[2],
        b3 = this.matrix[3];
      this.matrix[0] = b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12];
      this.matrix[1] = b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13];
      this.matrix[2] =
        b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14];
      this.matrix[3] =
        b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15];

      b0 = this.matrix[4];
      b1 = this.matrix[5];
      b2 = this.matrix[6];
      b3 = this.matrix[7];
      this.matrix[4] = b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12];
      this.matrix[5] = b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13];
      this.matrix[6] =
        b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14];
      this.matrix[7] =
        b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15];

      b0 = this.matrix[8];
      b1 = this.matrix[9];
      b2 = this.matrix[10];
      b3 = this.matrix[11];
      this.matrix[8] = b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12];
      this.matrix[9] = b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13];
      this.matrix[10] =
        b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14];
      this.matrix[11] =
        b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15];

      b0 = this.matrix[12];
      b1 = this.matrix[13];
      b2 = this.matrix[14];
      b3 = this.matrix[15];
      this.matrix[12] =
        b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12];
      this.matrix[13] =
        b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13];
      this.matrix[14] =
        b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14];
      this.matrix[15] =
        b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15];

      return this;
    }

    /**
     * @param {p5.Matrix|Float32Array|Number[]} multMatrix The matrix
     *                                                we want to multiply by
     * @private
     * @chainable
     */
    #multNxN(multMatrix) {
      if (multMatrix.length !== this.matrix.length) {
        throw new Error('Matrices must be of the same dimension to multiply.');
      }
      const result = new GLMAT_ARRAY_TYPE(this.matrix.length).fill(0);
      for (let i = 0; i < this.#sqDimention; i++) {
        for (let j = 0; j < this.#sqDimention; j++) {
          for (let k = 0; k < this.#sqDimention; k++) {
            result[i * this.#sqDimention + j] +=
              this.matrix[i * this.#sqDimention + k] *
              multMatrix[k * this.#sqDimention + j];
          }
        }
      }
      this.matrix = result;
      return this;
    }

    /**
     * This function is only for 3x3 matrices.
     * multiply two mat3s. It is an operation to multiply the 3x3 matrix of
     * the argument from the right. Arguments can be a 3x3 p5.Matrix,
     * a Float32Array of length 9, or a javascript array of length 9.
     * In addition, it can also be done by enumerating 9 numbers.
     *
     * @param {p5.Matrix|Float32Array|Number[]} multMatrix The matrix
     *                                                we want to multiply by
     * @private
     * @chainable
     */
    #mult3x3(_src) {
      // each row is used for the multiplier
      let b0 = this.mat3[0];
      let b1 = this.mat3[1];
      let b2 = this.mat3[2];
      this.mat3[0] = b0 * _src[0] + b1 * _src[3] + b2 * _src[6];
      this.mat3[1] = b0 * _src[1] + b1 * _src[4] + b2 * _src[7];
      this.mat3[2] = b0 * _src[2] + b1 * _src[5] + b2 * _src[8];

      b0 = this.mat3[3];
      b1 = this.mat3[4];
      b2 = this.mat3[5];
      this.mat3[3] = b0 * _src[0] + b1 * _src[3] + b2 * _src[6];
      this.mat3[4] = b0 * _src[1] + b1 * _src[4] + b2 * _src[7];
      this.mat3[5] = b0 * _src[2] + b1 * _src[5] + b2 * _src[8];

      b0 = this.mat3[6];
      b1 = this.mat3[7];
      b2 = this.mat3[8];
      this.mat3[6] = b0 * _src[0] + b1 * _src[3] + b2 * _src[6];
      this.mat3[7] = b0 * _src[1] + b1 * _src[4] + b2 * _src[7];
      this.mat3[8] = b0 * _src[2] + b1 * _src[5] + b2 * _src[8];

      return this;
    }

    /**
     * Transposes a square matrix in place.
     * This method swaps the rows and columns of the matrix, effectively flipping it over its diagonal.
     *
     * @private
     * @returns {Matrix} The current instance of the Matrix, with the transposed values.
     */
    #transposeNxN() {
      const n = this.#sqDimention;
      for (let i = 0; i < n; i++) {
        for (let j = 0; j < n; j++) {
          this.matrix[i * n + j] = this.matrix[j * n + i];
        }
      }
      return this;
    }

    /**
     * transpose according to a given matrix
     * @param  {p5.Matrix|Float32Array|Number[]} a  the matrix to be
     *                                               based on to transpose
     * @private
     * @chainable
     */
    #transpose4x4(a) {
      console.log('====> 4x4');
      let a01, a02, a03, a12, a13, a23;
      if (a instanceof Matrix) {
        a01 = a.matrix[1];
        a02 = a.matrix[2];
        a03 = a.matrix[3];
        a12 = a.matrix[6];
        a13 = a.matrix[7];
        a23 = a.matrix[11];

        this.matrix[0] = a.matrix[0];
        this.matrix[1] = a.matrix[4];
        this.matrix[2] = a.matrix[8];
        this.matrix[3] = a.matrix[12];
        this.matrix[4] = a01;
        this.matrix[5] = a.matrix[5];
        this.matrix[6] = a.matrix[9];
        this.matrix[7] = a.matrix[13];
        this.matrix[8] = a02;
        this.matrix[9] = a12;
        this.matrix[10] = a.matrix[10];
        this.matrix[11] = a.matrix[14];
        this.matrix[12] = a03;
        this.matrix[13] = a13;
        this.matrix[14] = a23;
        this.matrix[15] = a.matrix[15];
      } else if (isMatrixArray(a)) {
        a01 = a[1];
        a02 = a[2];
        a03 = a[3];
        a12 = a[6];
        a13 = a[7];
        a23 = a[11];

        this.matrix[0] = a[0];
        this.matrix[1] = a[4];
        this.matrix[2] = a[8];
        this.matrix[3] = a[12];
        this.matrix[4] = a01;
        this.matrix[5] = a[5];
        this.matrix[6] = a[9];
        this.matrix[7] = a[13];
        this.matrix[8] = a02;
        this.matrix[9] = a12;
        this.matrix[10] = a[10];
        this.matrix[11] = a[14];
        this.matrix[12] = a03;
        this.matrix[13] = a13;
        this.matrix[14] = a23;
        this.matrix[15] = a[15];
      }
      return this;
    }

    /**
     * This function is only for 3x3 matrices.
     * transposes a 3×3 p5.Matrix by a mat3
     * If there is an array of arguments, the matrix obtained by transposing
     * the 3x3 matrix generated based on that array is set.
     * If no arguments, it transposes itself and returns it.
     *
     * @param  {Number[]} mat3 1-dimensional array
     * @private
     * @chainable
     */
    #transpose3x3(mat3) {
      if (mat3 === undefined) {
        mat3 = this.mat3;
      }
      const a01 = mat3[1];
      const a02 = mat3[2];
      const a12 = mat3[5];
      this.mat3[0] = mat3[0];
      this.mat3[1] = mat3[3];
      this.mat3[2] = mat3[6];
      this.mat3[3] = a01;
      this.mat3[4] = mat3[4];
      this.mat3[5] = mat3[7];
      this.mat3[6] = a02;
      this.mat3[7] = a12;
      this.mat3[8] = mat3[8];

      return this;
    }

    /**
     * Only 4x4 becasuse determinant is only 4x4 currently
     * invert  matrix according to a give matrix
     * @param  {p5.Matrix|Float32Array|Number[]} a   the matrix to be
     *                                                based on to invert
     * @private
     * @chainable
     */
    #invert4x4(a) {
      let a00, a01, a02, a03, a10, a11, a12, a13;
      let a20, a21, a22, a23, a30, a31, a32, a33;
      if (a instanceof Matrix) {
        a00 = a.matrix[0];
        a01 = a.matrix[1];
        a02 = a.matrix[2];
        a03 = a.matrix[3];
        a10 = a.matrix[4];
        a11 = a.matrix[5];
        a12 = a.matrix[6];
        a13 = a.matrix[7];
        a20 = a.matrix[8];
        a21 = a.matrix[9];
        a22 = a.matrix[10];
        a23 = a.matrix[11];
        a30 = a.matrix[12];
        a31 = a.matrix[13];
        a32 = a.matrix[14];
        a33 = a.matrix[15];
      } else if (isMatrixArray(a)) {
        a00 = a[0];
        a01 = a[1];
        a02 = a[2];
        a03 = a[3];
        a10 = a[4];
        a11 = a[5];
        a12 = a[6];
        a13 = a[7];
        a20 = a[8];
        a21 = a[9];
        a22 = a[10];
        a23 = a[11];
        a30 = a[12];
        a31 = a[13];
        a32 = a[14];
        a33 = a[15];
      }
      const b00 = a00 * a11 - a01 * a10;
      const b01 = a00 * a12 - a02 * a10;
      const b02 = a00 * a13 - a03 * a10;
      const b03 = a01 * a12 - a02 * a11;
      const b04 = a01 * a13 - a03 * a11;
      const b05 = a02 * a13 - a03 * a12;
      const b06 = a20 * a31 - a21 * a30;
      const b07 = a20 * a32 - a22 * a30;
      const b08 = a20 * a33 - a23 * a30;
      const b09 = a21 * a32 - a22 * a31;
      const b10 = a21 * a33 - a23 * a31;
      const b11 = a22 * a33 - a23 * a32;

      // Calculate the determinant
      let det =
        b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;

      if (!det) {
        return null;
      }
      det = 1.0 / det;

      this.matrix[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
      this.matrix[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
      this.matrix[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det;
      this.matrix[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det;
      this.matrix[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det;
      this.matrix[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det;
      this.matrix[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det;
      this.matrix[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det;
      this.matrix[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det;
      this.matrix[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det;
      this.matrix[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det;
      this.matrix[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det;
      this.matrix[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det;
      this.matrix[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det;
      this.matrix[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det;
      this.matrix[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det;

      return this;
    }

    /**
     * Inverts a 3×3 matrix
     * @chainable
     * @private
     */
    #invert3x3() {
      const a00 = this.mat3[0];
      const a01 = this.mat3[1];
      const a02 = this.mat3[2];
      const a10 = this.mat3[3];
      const a11 = this.mat3[4];
      const a12 = this.mat3[5];
      const a20 = this.mat3[6];
      const a21 = this.mat3[7];
      const a22 = this.mat3[8];
      const b01 = a22 * a11 - a12 * a21;
      const b11 = -a22 * a10 + a12 * a20;
      const b21 = a21 * a10 - a11 * a20;

      // Calculate the determinant
      let det = a00 * b01 + a01 * b11 + a02 * b21;
      if (!det) {
        return null;
      }
      det = 1.0 / det;
      this.mat3[0] = b01 * det;
      this.mat3[1] = (-a22 * a01 + a02 * a21) * det;
      this.mat3[2] = (a12 * a01 - a02 * a11) * det;
      this.mat3[3] = b11 * det;
      this.mat3[4] = (a22 * a00 - a02 * a20) * det;
      this.mat3[5] = (-a12 * a00 + a02 * a10) * det;
      this.mat3[6] = b21 * det;
      this.mat3[7] = (-a21 * a00 + a01 * a20) * det;
      this.mat3[8] = (a11 * a00 - a01 * a10) * det;
      return this;
    }

    /**
     * inspired by Toji's mat4 determinant
     * @return {Number} Determinant of our 4×4 matrix
     * @private
     */
    #determinant4x4() {
      if (this.#sqDimention !== 4) {
        throw new Error(
          'Determinant is only implemented for 4x4 matrices. We are working on it.'
        );
      }

      const d00 =
          this.matrix[0] * this.matrix[5] - this.matrix[1] * this.matrix[4],
        d01 = this.matrix[0] * this.matrix[6] - this.matrix[2] * this.matrix[4],
        d02 = this.matrix[0] * this.matrix[7] - this.matrix[3] * this.matrix[4],
        d03 = this.matrix[1] * this.matrix[6] - this.matrix[2] * this.matrix[5],
        d04 = this.matrix[1] * this.matrix[7] - this.matrix[3] * this.matrix[5],
        d05 = this.matrix[2] * this.matrix[7] - this.matrix[3] * this.matrix[6],
        d06 = this.matrix[8] * this.matrix[13] - this.matrix[9] * this.matrix[12],
        d07 =
          this.matrix[8] * this.matrix[14] - this.matrix[10] * this.matrix[12],
        d08 =
          this.matrix[8] * this.matrix[15] - this.matrix[11] * this.matrix[12],
        d09 =
          this.matrix[9] * this.matrix[14] - this.matrix[10] * this.matrix[13],
        d10 =
          this.matrix[9] * this.matrix[15] - this.matrix[11] * this.matrix[13],
        d11 =
          this.matrix[10] * this.matrix[15] - this.matrix[11] * this.matrix[14];

      // Calculate the determinant
      return (
        d00 * d11 - d01 * d10 + d02 * d09 + d03 * d08 - d04 * d07 + d05 * d06
      );
    }

    /**
     * PRIVATE
     */
    // matrix methods adapted from:
    // https://developer.mozilla.org/en-US/docs/Web/WebGL/
    // gluPerspective
    //
    // function _makePerspective(fovy, aspect, znear, zfar){
    //    const ymax = znear * Math.tan(fovy * Math.PI / 360.0);
    //    const ymin = -ymax;
    //    const xmin = ymin * aspect;
    //    const xmax = ymax * aspect;
    //    return _makeFrustum(xmin, xmax, ymin, ymax, znear, zfar);
    //  }

    ////
    //// glFrustum
    ////
    //function _makeFrustum(left, right, bottom, top, znear, zfar){
    //  const X = 2*znear/(right-left);
    //  const Y = 2*znear/(top-bottom);
    //  const A = (right+left)/(right-left);
    //  const B = (top+bottom)/(top-bottom);
    //  const C = -(zfar+znear)/(zfar-znear);
    //  const D = -2*zfar*znear/(zfar-znear);
    //  const frustrumMatrix =[
    //  X, 0, A, 0,
    //  0, Y, B, 0,
    //  0, 0, C, D,
    //  0, 0, -1, 0
    //];
    //return frustrumMatrix;
    // }

    // function _setMVPMatrices(){
    ////an identity matrix
    ////@TODO use the p5.Matrix class to abstract away our MV matrices and
    ///other math
    //const _mvMatrix =
    //[
    //  1.0,0.0,0.0,0.0,
    //  0.0,1.0,0.0,0.0,
    //  0.0,0.0,1.0,0.0,
    //  0.0,0.0,0.0,1.0
    //];
  }

  /**
   * @module Math
   * @requires constants
   * @todo see methods below needing further implementation.
   * future consideration: implement SIMD optimizations
   * when browser compatibility becomes available
   * https://developer.mozilla.org/en-US/docs/Web/JavaScript/
   *   Reference/Global_Objects/SIMD
   */
  // import { MatrixNumjs as Matrix } from './Matrices/MatrixNumjs'

  function matrix(p5, fn) {
    /**
     * A class to describe a matrix
     * for model and view matrix manipulation in the p5js webgl renderer.
     * The `Matrix` class represents a mathematical matrix and provides various methods for matrix operations.
     *
     * The `Matrix` class represents a mathematical matrix and provides various methods for matrix operations.
     * This class extends the `MatrixInterface` and includes methods for creating, manipulating, and performing
     * operations on matrices. It supports both 3x3 and 4x4 matrices, as well as general NxN matrices.
     * @private
     * @class p5.Matrix
     * @param {Array} [mat4] column-major array literal of our 4×4 matrix
     * @example
     * // Creating a 3x3 matrix from an array using column major arrangement
     * const matrix = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     *
     * // Creating a 4x4 identity matrix
     * const identityMatrix = new p5.Matrix(4);
     *
     * // Adding two matrices
     * const matrix1 = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     * const matrix2 = new p5.Matrix([9, 8, 7, 6, 5, 4, 3, 2, 1]);
     * matrix1.add(matrix2); // matrix1 is now [10, 10, 10, 10, 10, 10, 10, 10, 10]
     *
     * // Setting an element in the matrix
     * matrix.setElement(0, 10); // matrix is now [10, 2, 3, 4, 5, 6, 7, 8, 9]
     *
     * // Resetting the matrix to an identity matrix
     * matrix.reset();
     *
     * // Getting the diagonal elements of the matrix
     * const diagonal = matrix.diagonal(); // [1, 1, 1]
     *
     * // Transposing the matrix
     * matrix.transpose();
     *
     * // Multiplying two matrices
     * matrix1.mult(matrix2);
     *
     * // Inverting the matrix
     * matrix.invert();
     *
     * // Scaling the matrix
     * matrix.scale(2, 2, 2);
     *
     * // Rotating the matrix around an axis
     * matrix.rotate4x4(Math.PI / 4, 1, 0, 0);
     *
     * // Applying a perspective transformation
     * matrix.perspective(Math.PI / 4, 1, 0.1, 100);
     *
     * // Applying an orthographic transformation
     * matrix.ortho(-1, 1, -1, 1, 0.1, 100);
     *
     * // Multiplying a vector by the matrix
     * const vector = new Vector(1, 2, 3);
     * const result = matrix.multiplyPoint(vector);
     *
     * @example
     * // META:norender
     * // p5.js script example
     * function setup() {
     *
     *   // Create a 4x4 identity matrix
     *   const matrix = new p5.Matrix(4);
     *   console.log("Original p5.Matrix:", matrix.matrix.toString()); // Output: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
     *
     *   // Add two matrices
     *   const matrix1 = new p5.Matrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     *   const matrix2 = new p5.Matrix([9, 8, 7, 6, 5, 4, 3, 2, 1]);
     *   matrix1.add(matrix2);
     *   console.log("After Addition:", matrix1.matrix.toString()); // Output: [10, 10, 10, 10, 10, 10, 10, 10, 10]
     *
     *   // Reset the matrix to an identity matrix
     *   matrix.reset();
     *   console.log("Reset p5.Matrix:", matrix.matrix.toString()); // [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
     *
     *   // Apply a scaling transformation
     *   matrix.scale(2, 2, 2);
     *   console.log("Scaled p5.Matrix:", matrix.matrix.toString()); // [2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1]
     *
     *   // Apply a rotation around the X-axis
     *   matrix.rotate4x4(Math.PI / 4, 1, 0, 0);
     *   console.log("Rotated p5.Matrix (X-axis):", matrix.matrix.toString()); // [2, 0, 0, 0, 0, 1.4142135381698608, 1.4142135381698608, 0, 0, -1.4142135381698608, 1.4142135381698608, 0, 0, 0, 0, 1]
     *
     *   // Apply a perspective transformation
     *   matrix.perspective(Math.PI / 4, 1, 0.1, 100);
     *   console.log("Perspective p5.Matrix:", matrix.matrix.toString());// [2.4142136573791504, 0, 0, 0, 0, 2.4142136573791504, 0, 0, 0, 0, -1.0020020008087158, -1, 0, 0, -0.20020020008087158, 0]
     *
     *   // Multiply a vector by the matrix
     *   const vector = new p5.Vector(1, 2, 3);
     *   const transformedVector = matrix.multiplyPoint(vector);
     *   console.log("Transformed Vector:", transformedVector.toString());
     * }
     */
    p5.Matrix = Matrix;
  }

  if (typeof p5 !== 'undefined') {
    matrix(p5);
  }

  class DataArray {
    constructor(initialLength = 128) {
      this.length = 0;
      this.data = new Float32Array(initialLength);
      this.initialLength = initialLength;
    }

    /**
     * Returns a Float32Array window sized to the exact length of the data
     */
    dataArray() {
      return this.subArray(0, this.length);
    }

    /**
     * A "soft" clear, which keeps the underlying storage size the same, but
     * empties the contents of its dataArray()
     */
    clear() {
      this.length = 0;
    }

    /**
     * Can be used to scale a DataArray back down to fit its contents.
     */
    rescale() {
      if (this.length < this.data.length / 2) {
        // Find the power of 2 size that fits the data
        const targetLength = 1 << Math.ceil(Math.log2(this.length));
        const newData = new Float32Array(targetLength);
        newData.set(this.data.subarray(0, this.length), 0);
        this.data = newData;
      }
    }

    /**
     * A full reset, which allocates a new underlying Float32Array at its initial
     * length
     */
    reset() {
      this.clear();
      this.data = new Float32Array(this.initialLength);
    }

    /**
     * Adds values to the DataArray, expanding its internal storage to
     * accommodate the new items.
     */
    push(...values) {
      this.ensureLength(this.length + values.length);
      this.data.set(values, this.length);
      this.length += values.length;
    }

    /**
     * Returns a copy of the data from the index `from`, inclusive, to the index
     * `to`, exclusive
     */
    slice(from, to) {
      return this.data.slice(from, Math.min(to, this.length));
    }

    /**
     * Returns a mutable Float32Array window from the index `from`, inclusive, to
     * the index `to`, exclusive
     */
    subArray(from, to) {
      return this.data.subarray(from, Math.min(to, this.length));
    }

    /**
     * Expand capacity of the internal storage until it can fit a target size
     */
    ensureLength(target) {
      while (this.data.length < target) {
        const newData = new Float32Array(this.data.length * 2);
        newData.set(this.data, 0);
        this.data = newData;
      }
    }
  }
  function dataArray(p5, fn){
    /**
     * An internal class to store data that will be sent to a p5.RenderBuffer.
     * Those need to eventually go into a Float32Array, so this class provides a
     * variable-length array container backed by a Float32Array so that it can be
     * sent to the GPU without allocating a new array each frame.
     *
     * Like a C++ vector, its fixed-length Float32Array backing its contents will
     * double in size when it goes over its capacity.
     *
     * @example
     * // Initialize storage with a capacity of 4
     * const storage = new DataArray(4);
     * console.log(storage.data.length); // 4
     * console.log(storage.length); // 0
     * console.log(storage.dataArray()); // Empty Float32Array
     *
     * storage.push(1, 2, 3, 4, 5, 6);
     * console.log(storage.data.length); // 8
     * console.log(storage.length); // 6
     * console.log(storage.dataArray()); // Float32Array{1, 2, 3, 4, 5, 6}
     */
    p5.DataArray = DataArray;
  }

  if(typeof p5 !== 'undefined'){
    dataArray(p5);
  }

  /**
   * @module Shape
   * @submodule 3D Primitives
   * @for p5
   * @requires core
   * @requires p5.Geometry
   */


  class Geometry {
    constructor(detailX, detailY, callback, renderer) {
      this.renderer = renderer;
      this.vertices = [];

      this.boundingBoxCache = null;


      //an array containing every vertex for stroke drawing
      this.lineVertices = new DataArray();

      // The tangents going into or out of a vertex on a line. Along a straight
      // line segment, both should be equal. At an endpoint, one or the other
      // will not exist and will be all 0. In joins between line segments, they
      // may be different, as they will be the tangents on either side of the join.
      this.lineTangentsIn = new DataArray();
      this.lineTangentsOut = new DataArray();

      // When drawing lines with thickness, entries in this buffer represent which
      // side of the centerline the vertex will be placed. The sign of the number
      // will represent the side of the centerline, and the absolute value will be
      // used as an enum to determine which part of the cap or join each vertex
      // represents. See the doc comments for _addCap and _addJoin for diagrams.
      this.lineSides = new DataArray();

      this.vertexNormals = [];

      this.faces = [];

      this.uvs = [];
      // a 2D array containing edge connectivity pattern for create line vertices
      //based on faces for most objects;
      this.edges = [];
      this.vertexColors = [];

      // One color per vertex representing the stroke color at that vertex
      this.vertexStrokeColors = [];

      this.userVertexProperties = {};

      // One color per line vertex, generated automatically based on
      // vertexStrokeColors in _edgesToVertices()
      this.lineVertexColors = new DataArray();
      this.detailX = detailX !== undefined ? detailX : 1;
      this.detailY = detailY !== undefined ? detailY : 1;
      this.dirtyFlags = {};

      this._hasFillTransparency = undefined;
      this._hasStrokeTransparency = undefined;

      this.gid = `_p5_Geometry_${Geometry.nextId}`;
      Geometry.nextId++;
      if (callback instanceof Function) {
        callback.call(this);
      }
    }

    /**
     * Calculates the position and size of the smallest box that contains the geometry.
     *
     * A bounding box is the smallest rectangular prism that contains the entire
     * geometry. It's defined by the box's minimum and maximum coordinates along
     * each axis, as well as the size (length) and offset (center).
     *
     * Calling `myGeometry.calculateBoundingBox()` returns an object with four
     * properties that describe the bounding box:
     *
     * ```js
     * // Get myGeometry's bounding box.
     * let bbox = myGeometry.calculateBoundingBox();
     *
     * // Print the bounding box to the console.
     * console.log(bbox);
     *
     * // {
     * //  // The minimum coordinate along each axis.
     * //  min: { x: -1, y: -2, z: -3 },
     * //
     * //  // The maximum coordinate along each axis.
     * //  max: { x: 1, y: 2, z: 3},
     * //
     * //  // The size (length) along each axis.
     * //  size: { x: 2, y: 4, z: 6},
     * //
     * //  // The offset (center) along each axis.
     * //  offset: { x: 0, y: 0, z: 0}
     * // }
     * ```
     *
     * @returns {Object} bounding box of the geometry.
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let particles;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a new p5.Geometry object with random spheres.
     *   particles = buildGeometry(createParticles);
     *
     *   describe('Ten white spheres placed randomly against a gray background. A box encloses the spheres.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Style the particles.
     *   noStroke();
     *   fill(255);
     *
     *   // Draw the particles.
     *   model(particles);
     *
     *   // Calculate the bounding box.
     *   let bbox = particles.calculateBoundingBox();
     *
     *   // Translate to the bounding box's center.
     *   translate(bbox.offset.x, bbox.offset.y, bbox.offset.z);
     *
     *   // Style the bounding box.
     *   stroke(255);
     *   noFill();
     *
     *   // Draw the bounding box.
     *   box(bbox.size.x, bbox.size.y, bbox.size.z);
     * }
     *
     * function createParticles() {
     *   for (let i = 0; i < 10; i += 1) {
     *     // Calculate random coordinates.
     *     let x = randomGaussian(0, 15);
     *     let y = randomGaussian(0, 15);
     *     let z = randomGaussian(0, 15);
     *
     *     push();
     *     // Translate to the particle's coordinates.
     *     translate(x, y, z);
     *     // Draw the particle.
     *     sphere(3);
     *     pop();
     *   }
     * }
     */
    calculateBoundingBox() {
      if (this.boundingBoxCache) {
        return this.boundingBoxCache; // Return cached result if available
      }

      let minVertex = new Vector(
        Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
      let maxVertex = new Vector(
        Number.MIN_VALUE, Number.MIN_VALUE, Number.MIN_VALUE);

      for (let i = 0; i < this.vertices.length; i++) {
        let vertex = this.vertices[i];
        minVertex.x = Math.min(minVertex.x, vertex.x);
        minVertex.y = Math.min(minVertex.y, vertex.y);
        minVertex.z = Math.min(minVertex.z, vertex.z);

        maxVertex.x = Math.max(maxVertex.x, vertex.x);
        maxVertex.y = Math.max(maxVertex.y, vertex.y);
        maxVertex.z = Math.max(maxVertex.z, vertex.z);
      }
      // Calculate size and offset properties
      let size = new Vector(maxVertex.x - minVertex.x,
        maxVertex.y - minVertex.y, maxVertex.z - minVertex.z);
      let offset = new Vector((minVertex.x + maxVertex.x) / 2,
        (minVertex.y + maxVertex.y) / 2, (minVertex.z + maxVertex.z) / 2);

      // Cache the result for future access
      this.boundingBoxCache = {
        min: minVertex,
        max: maxVertex,
        size: size,
        offset: offset
      };

      return this.boundingBoxCache;
    }

    reset() {
      // Notify renderer that geometry is being reset (for buffer cleanup)
      this.renderer?.onReset?.(this);

      this._hasFillTransparency = undefined;
      this._hasStrokeTransparency = undefined;

      this.lineVertices.clear();
      this.lineTangentsIn.clear();
      this.lineTangentsOut.clear();
      this.lineSides.clear();

      this.vertices.length = 0;
      this.edges.length = 0;
      this.vertexColors.length = 0;
      this.vertexStrokeColors.length = 0;
      this.lineVertexColors.clear();
      this.vertexNormals.length = 0;
      this.uvs.length = 0;

      for (const propName in this.userVertexProperties){
        this.userVertexProperties[propName].delete();
      }
      this.userVertexProperties = {};

      this.dirtyFlags = {};
    }

    hasFillTransparency() {
      if (this._hasFillTransparency === undefined) {
        this._hasFillTransparency = false;
        for (let i = 0; i < this.vertexColors.length; i += 4) {
          if (this.vertexColors[i + 3] < 1) {
            this._hasFillTransparency = true;
            break;
          }
        }
      }
      return this._hasFillTransparency;
    }
    hasStrokeTransparency() {
      if (this._hasStrokeTransparency === undefined) {
        this._hasStrokeTransparency = false;
        for (let i = 0; i < this.lineVertexColors.length; i += 4) {
          if (this.lineVertexColors[i + 3] < 1) {
            this._hasStrokeTransparency = true;
            break;
          }
        }
      }
      return this._hasStrokeTransparency;
    }

    /**
     * Removes the geometry’s internal colors.
     *
     * `p5.Geometry` objects can be created with "internal colors" assigned to
     * vertices or the entire shape. When a geometry has internal colors,
     * <a href="#/p5/fill">fill()</a> has no effect. Calling
     * `myGeometry.clearColors()` allows the
     * <a href="#/p5/fill">fill()</a> function to apply color to the geometry.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   // Create a p5.Geometry object.
     *   // Set its internal color to red.
     *   let myGeometry = buildGeometry(function() {
     *     fill(255, 0, 0);
     *     plane(20);
     *   });
     *
     *   // Style the shape.
     *   noStroke();
     *
     *   // Draw the p5.Geometry object (center).
     *   model(myGeometry);
     *
     *   // Translate the origin to the bottom-right.
     *   translate(25, 25, 0);
     *
     *   // Try to fill the geometry with green.
     *   fill(0, 255, 0);
     *
     *   // Draw the geometry again (bottom-right).
     *   model(myGeometry);
     *
     *   // Clear the geometry's colors.
     *   myGeometry.clearColors();
     *
     *   // Fill the geometry with blue.
     *   fill(0, 0, 255);
     *
     *   // Translate the origin up.
     *   translate(0, -50, 0);
     *
     *   // Draw the geometry again (top-right).
     *   model(myGeometry);
     *
     *   describe(
     *     'Three squares drawn against a gray background. Red squares are at the center and the bottom-right. A blue square is at the top-right.'
     *   );
     * }
     */
    clearColors() {
      this.vertexColors = [];
      return this;
    }

    /**
     * The `saveObj()` function exports `p5.Geometry` objects as
     * 3D models in the Wavefront .obj file format.
     * This way, you can use the 3D shapes you create in p5.js in other software
     * for rendering, animation, 3D printing, or more.
     *
     * The exported .obj file will include the faces and vertices of the `p5.Geometry`,
     * as well as its texture coordinates and normals, if it has them.
     *
     * @method saveObj
     * @param {String} [fileName='model.obj'] The name of the file to save the model as.
     *                                        If not specified, the default file name will be 'model.obj'.
     * @example
     * let myModel;
     * let saveBtn;
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myModel = buildGeometry(function()) {
     *     for (let i = 0; i < 5; i++) {
     *       push();
     *       translate(
     *         random(-75, 75),
     *         random(-75, 75),
     *         random(-75, 75)
     *       );
     *       sphere(random(5, 50));
     *       pop();
     *     }
     *   });
     *
     *   saveBtn = createButton('Save .obj');
     *   saveBtn.mousePressed(() => myModel.saveObj());
     *
     *   describe('A few spheres rotating in space');
     * }
     *
     * function draw() {
     *   background(0);
     *   noStroke();
     *   lights();
     *   rotateX(millis() * 0.001);
     *   rotateY(millis() * 0.002);
     *   model(myModel);
     * }
     */
    saveObj(fileName = 'model.obj') {
      let objStr= '';


      // Vertices
      this.vertices.forEach(v => {
        objStr += `v ${v.x} ${v.y} ${v.z}\n`;
      });

      // Texture Coordinates (UVs)
      if (this.uvs && this.uvs.length > 0) {
        for (let i = 0; i < this.uvs.length; i += 2) {
          objStr += `vt ${this.uvs[i]} ${this.uvs[i + 1]}\n`;
        }
      }

      // Vertex Normals
      if (this.vertexNormals && this.vertexNormals.length > 0) {
        this.vertexNormals.forEach(n => {
          objStr += `vn ${n.x} ${n.y} ${n.z}\n`;
        });

      }
      // Faces, obj vertex indices begin with 1 and not 0
      // texture coordinate (uvs) and vertexNormal indices
      // are indicated with trailing ints vertex/normal/uv
      // ex 1/1/1 or 2//2 for vertices without uvs
      this.faces.forEach(face => {
        let faceStr = 'f';
        face.forEach(index =>{
          faceStr += ' ';
          faceStr += index + 1;
          if (this.vertexNormals.length > 0 || this.uvs.length > 0) {
            faceStr += '/';
            if (this.uvs.length > 0) {
              faceStr += index + 1;
            }
            faceStr += '/';
            if (this.vertexNormals.length > 0) {
              faceStr += index + 1;
            }
          }
        });
        objStr += faceStr + '\n';
      });

      const blob = new Blob([objStr], { type: 'text/plain' });
      downloadFile(blob, fileName , 'obj');

    }

    /**
     * The `saveStl()` function exports `p5.Geometry` objects as
     * 3D models in the STL stereolithography file format.
     * This way, you can use the 3D shapes you create in p5.js in other software
     * for rendering, animation, 3D printing, or more.
     *
     * The exported .stl file will include the faces, vertices, and normals of the `p5.Geometry`.
     *
     * By default, this method saves a text-based .stl file. Alternatively, you can save a more compact
     * but less human-readable binary .stl file by passing `{ binary: true }` as a second parameter.
     *
     * @method saveStl
     * @param {String} [fileName='model.stl'] The name of the file to save the model as.
     *                                        If not specified, the default file name will be 'model.stl'.
     * @param {Object} [options] Optional settings.
     * @param {Boolean} [options.binary=false] Whether or not a binary .stl file is saved.
     * @example
     * let myModel;
     * let saveBtn1;
     * let saveBtn2;
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myModel = buildGeometry(function() {
     *     for (let i = 0; i < 5; i++) {
     *       push();
     *       translate(
     *         random(-75, 75),
     *         random(-75, 75),
     *         random(-75, 75)
     *       );
     *       sphere(random(5, 50));
     *       pop();
     *     }
     *   });
     *
     *   saveBtn1 = createButton('Save .stl');
     *   saveBtn1.mousePressed(function() {
     *     myModel.saveStl();
     *   });
     *   saveBtn2 = createButton('Save binary .stl');
     *   saveBtn2.mousePressed(function() {
     *     myModel.saveStl('model.stl', { binary: true });
     *   });
     *
     *   describe('A few spheres rotating in space');
     * }
     *
     * function draw() {
     *   background(0);
     *   noStroke();
     *   lights();
     *   rotateX(millis() * 0.001);
     *   rotateY(millis() * 0.002);
     *   model(myModel);
     * }
     */
    saveStl(fileName = 'model.stl', { binary = false } = {}){
      let modelOutput;
      let name = fileName.substring(0, fileName.lastIndexOf('.'));
      let faceNormals = [];
      for (let f of this.faces) {
        const U = Vector.sub(this.vertices[f[1]], this.vertices[f[0]]);
        const V = Vector.sub(this.vertices[f[2]], this.vertices[f[0]]);
        const nx = U.y * V.z - U.z * V.y;
        const ny = U.z * V.x - U.x * V.z;
        const nz = U.x * V.y - U.y * V.x;
        faceNormals.push(new Vector(nx, ny, nz).normalize());
      }
      if (binary) {
        let offset = 80;
        const bufferLength =
          this.faces.length * 2 + this.faces.length * 3 * 4 * 4 + 80 + 4;
        const arrayBuffer = new ArrayBuffer(bufferLength);
        modelOutput = new DataView(arrayBuffer);
        modelOutput.setUint32(offset, this.faces.length, true);
        offset += 4;
        for (const [key, f] of Object.entries(this.faces)) {
          const norm = faceNormals[key];
          modelOutput.setFloat32(offset, norm.x, true);
          offset += 4;
          modelOutput.setFloat32(offset, norm.y, true);
          offset += 4;
          modelOutput.setFloat32(offset, norm.z, true);
          offset += 4;
          for (let vertexIndex of f) {
            const vert = this.vertices[vertexIndex];
            modelOutput.setFloat32(offset, vert.x, true);
            offset += 4;
            modelOutput.setFloat32(offset, vert.y, true);
            offset += 4;
            modelOutput.setFloat32(offset, vert.z, true);
            offset += 4;
          }
          modelOutput.setUint16(offset, 0, true);
          offset += 2;
        }
      } else {
        modelOutput = 'solid ' + name + '\n';

        for (const [key, f] of Object.entries(this.faces)) {
          const norm = faceNormals[key];
          modelOutput +=
            ' facet norm ' + norm.x + ' ' + norm.y + ' ' + norm.z + '\n';
          modelOutput += '  outer loop' + '\n';
          for (let vertexIndex of f) {
            const vert = this.vertices[vertexIndex];
            modelOutput +=
              '   vertex ' + vert.x + ' ' + vert.y + ' ' + vert.z + '\n';
          }
          modelOutput += '  endloop' + '\n';
          modelOutput += ' endfacet' + '\n';
        }
        modelOutput += 'endsolid ' + name + '\n';
      }
      const blob = new Blob([modelOutput], { type: 'text/plain' });
      downloadFile(blob, fileName, 'stl');
    }

    /**
     * Flips the geometry’s texture u-coordinates.
     *
     * In order for <a href="#/p5/texture">texture()</a> to work, the geometry
     * needs a way to map the points on its surface to the pixels in a rectangular
     * image that's used as a texture. The geometry's vertex at coordinates
     * `(x, y, z)` maps to the texture image's pixel at coordinates `(u, v)`.
     *
     * The <a href="#/p5.Geometry/uvs">myGeometry.uvs</a> array stores the
     * `(u, v)` coordinates for each vertex in the order it was added to the
     * geometry. Calling `myGeometry.flipU()` flips a geometry's u-coordinates
     * so that the texture appears mirrored horizontally.
     *
     * For example, a plane's four vertices are added clockwise starting from the
     * top-left corner. Here's how calling `myGeometry.flipU()` would change a
     * plane's texture coordinates:
     *
     * ```js
     * // Print the original texture coordinates.
     * // Output: [0, 0, 1, 0, 0, 1, 1, 1]
     * console.log(myGeometry.uvs);
     *
     * // Flip the u-coordinates.
     * myGeometry.flipU();
     *
     * // Print the flipped texture coordinates.
     * // Output: [1, 0, 0, 0, 1, 1, 0, 1]
     * console.log(myGeometry.uvs);
     *
     * // Notice the swaps:
     * // Top vertices: [0, 0, 1, 0] --> [1, 0, 0, 0]
     * // Bottom vertices: [0, 1, 1, 1] --> [1, 1, 0, 1]
     * ```
     *
     * @for p5.Geometry
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   img = await loadImage('assets/laDefense.jpg');
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   // Create p5.Geometry objects.
     *   let geom1 = buildGeometry(createShape);
     *   let geom2 = buildGeometry(createShape);
     *
     *   // Flip geom2's U texture coordinates.
     *   geom2.flipU();
     *
     *   // Left (original).
     *   push();
     *   translate(-25, 0, 0);
     *   texture(img);
     *   noStroke();
     *   model(geom1);
     *   pop();
     *
     *   // Right (flipped).
     *   push();
     *   translate(25, 0, 0);
     *   texture(img);
     *   noStroke();
     *   model(geom2);
     *   pop();
     *
     *   describe(
     *     'Two photos of a ceiling on a gray background. The photos are mirror images of each other.'
     *   );
     * }
     *
     * function createShape() {
     *   plane(40);
     * }
     */
    flipU() {
      this.uvs = this.uvs.flat().map((val, index) => {
        if (index % 2 === 0) {
          return 1 - val;
        } else {
          return val;
        }
      });
    }

    /**
     * Flips the geometry’s texture v-coordinates.
     *
     * In order for <a href="#/p5/texture">texture()</a> to work, the geometry
     * needs a way to map the points on its surface to the pixels in a rectangular
     * image that's used as a texture. The geometry's vertex at coordinates
     * `(x, y, z)` maps to the texture image's pixel at coordinates `(u, v)`.
     *
     * The <a href="#/p5.Geometry/uvs">myGeometry.uvs</a> array stores the
     * `(u, v)` coordinates for each vertex in the order it was added to the
     * geometry. Calling `myGeometry.flipV()` flips a geometry's v-coordinates
     * so that the texture appears mirrored vertically.
     *
     * For example, a plane's four vertices are added clockwise starting from the
     * top-left corner. Here's how calling `myGeometry.flipV()` would change a
     * plane's texture coordinates:
     *
     * ```js
     * // Print the original texture coordinates.
     * // Output: [0, 0, 1, 0, 0, 1, 1, 1]
     * console.log(myGeometry.uvs);
     *
     * // Flip the v-coordinates.
     * myGeometry.flipV();
     *
     * // Print the flipped texture coordinates.
     * // Output: [0, 1, 1, 1, 0, 0, 1, 0]
     * console.log(myGeometry.uvs);
     *
     * // Notice the swaps:
     * // Left vertices: [0, 0] <--> [1, 0]
     * // Right vertices: [1, 0] <--> [1, 1]
     * ```
     *
     * @method flipV
     * @for p5.Geometry
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   img = await loadImage('assets/laDefense.jpg');
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   // Create p5.Geometry objects.
     *   let geom1 = buildGeometry(createShape);
     *   let geom2 = buildGeometry(createShape);
     *
     *   // Flip geom2's V texture coordinates.
     *   geom2.flipV();
     *
     *   // Left (original).
     *   push();
     *   translate(-25, 0, 0);
     *   texture(img);
     *   noStroke();
     *   model(geom1);
     *   pop();
     *
     *   // Right (flipped).
     *   push();
     *   translate(25, 0, 0);
     *   texture(img);
     *   noStroke();
     *   model(geom2);
     *   pop();
     *
     *   describe(
     *     'Two photos of a ceiling on a gray background. The photos are mirror images of each other.'
     *   );
     * }
     *
     * function createShape() {
     *   plane(40);
     * }
     */
    flipV() {
      this.uvs = this.uvs.flat().map((val, index) => {
        if (index % 2 === 0) {
          return val;
        } else {
          return 1 - val;
        }
      });
    }

    /**
     * Computes the geometry's faces using its vertices.
     *
     * All 3D shapes are made by connecting sets of points called *vertices*. A
     * geometry's surface is formed by connecting vertices to form triangles that
     * are stitched together. Each triangular patch on the geometry's surface is
     * called a *face*. `myGeometry.computeFaces()` performs the math needed to
     * define each face based on the distances between vertices.
     *
     * The geometry's vertices are stored as <a href="#/p5.Vector">p5.Vector</a>
     * objects in the <a href="#/p5.Geometry/vertices">myGeometry.vertices</a>
     * array. The geometry's first vertex is the
     * <a href="#/p5.Vector">p5.Vector</a> object at `myGeometry.vertices[0]`,
     * its second vertex is `myGeometry.vertices[1]`, its third vertex is
     * `myGeometry.vertices[2]`, and so on.
     *
     * Calling `myGeometry.computeFaces()` fills the
     * <a href="#/p5.Geometry/faces">myGeometry.faces</a> array with three-element
     * arrays that list the vertices that form each face. For example, a geometry
     * made from a rectangle has two faces because a rectangle is made by joining
     * two triangles. <a href="#/p5.Geometry/faces">myGeometry.faces</a> for a
     * rectangle would be the two-dimensional array
     * `[[0, 1, 2], [2, 1, 3]]`. The first face, `myGeometry.faces[0]`, is the
     * array `[0, 1, 2]` because it's formed by connecting
     * `myGeometry.vertices[0]`, `myGeometry.vertices[1]`,and
     * `myGeometry.vertices[2]`. The second face, `myGeometry.faces[1]`, is the
     * array `[2, 1, 3]` because it's formed by connecting
     * `myGeometry.vertices[2]`, `myGeometry.vertices[1]`, and
     * `myGeometry.vertices[3]`.
     *
     * Note: `myGeometry.computeFaces()` only works when geometries have four or more vertices.
     *
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let myGeometry;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Geometry object.
     *   myGeometry = new p5.Geometry();
     *
     *   // Create p5.Vector objects to position the vertices.
     *   let v0 = createVector(-40, 0, 0);
     *   let v1 = createVector(0, -40, 0);
     *   let v2 = createVector(0, 40, 0);
     *   let v3 = createVector(40, 0, 0);
     *
     *   // Add the vertices to myGeometry's vertices array.
     *   myGeometry.vertices.push(v0, v1, v2, v3);
     *
     *   // Compute myGeometry's faces array.
     *   myGeometry.computeFaces();
     *
     *   describe('A red square drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Style the shape.
     *   noStroke();
     *   fill(255, 0, 0);
     *
     *   // Draw the p5.Geometry object.
     *   model(myGeometry);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let myGeometry;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Geometry object using a callback function.
     *   myGeometry = new p5.Geometry(1, 1, createShape);
     *
     *   describe('A red square drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Style the shape.
     *   noStroke();
     *   fill(255, 0, 0);
     *
     *   // Draw the p5.Geometry object.
     *   model(myGeometry);
     * }
     *
     * function createShape() {
     *   // Create p5.Vector objects to position the vertices.
     *   let v0 = createVector(-40, 0, 0);
     *   let v1 = createVector(0, -40, 0);
     *   let v2 = createVector(0, 40, 0);
     *   let v3 = createVector(40, 0, 0);
     *
     *   // Add the vertices to the p5.Geometry object's vertices array.
     *   this.vertices.push(v0, v1, v2, v3);
     *
     *   // Compute the faces array.
     *   this.computeFaces();
     * }
     */
    computeFaces() {
      this.faces.length = 0;
      const sliceCount = this.detailX + 1;
      let a, b, c, d;
      for (let i = 0; i < this.detailY; i++) {
        for (let j = 0; j < this.detailX; j++) {
          a = i * sliceCount + j; // + offset;
          b = i * sliceCount + j + 1; // + offset;
          c = (i + 1) * sliceCount + j + 1; // + offset;
          d = (i + 1) * sliceCount + j; // + offset;
          this.faces.push([a, b, d]);
          this.faces.push([d, b, c]);
        }
      }
      return this;
    }

    _getFaceNormal(faceId) {
      //This assumes that vA->vB->vC is a counter-clockwise ordering
      const face = this.faces[faceId];
      const vA = this.vertices[face[0]];
      const vB = this.vertices[face[1]];
      const vC = this.vertices[face[2]];
      const ab = Vector.sub(vB, vA);
      const ac = Vector.sub(vC, vA);
      const n = Vector.cross(ab, ac);
      const ln = Vector.mag(n);
      let sinAlpha = ln / (Vector.mag(ab) * Vector.mag(ac));
      if (sinAlpha === 0 || isNaN(sinAlpha)) {
        console.warn(
          'p5.Geometry.prototype._getFaceNormal:',
          'face has colinear sides or a repeated vertex'
        );
        return n;
      }
      if (sinAlpha > 1) sinAlpha = 1; // handle float rounding error
      return n.mult(Math.asin(sinAlpha) / ln);
    }
    /**
     * Calculates the normal vector for each vertex on the geometry.
     *
     * All 3D shapes are made by connecting sets of points called *vertices*. A
     * geometry's surface is formed by connecting vertices to create triangles
     * that are stitched together. Each triangular patch on the geometry's
     * surface is called a *face*. `myGeometry.computeNormals()` performs the
     * math needed to orient each face. Orientation is important for lighting
     * and other effects.
     *
     * A face's orientation is defined by its *normal vector* which points out
     * of the face and is normal (perpendicular) to the surface. Calling
     * `myGeometry.computeNormals()` first calculates each face's normal vector.
     * Then it calculates the normal vector for each vertex by averaging the
     * normal vectors of the faces surrounding the vertex. The vertex normals
     * are stored as <a href="#/p5.Vector">p5.Vector</a> objects in the
     * <a href="#/p5.Geometry/vertexNormals">myGeometry.vertexNormals</a> array.
     *
     * The first parameter, `shadingType`, is optional. Passing the constant
     * `FLAT`, as in `myGeometry.computeNormals(FLAT)`, provides neighboring
     * faces with their own copies of the vertices they share. Surfaces appear
     * tiled with flat shading. Passing the constant `SMOOTH`, as in
     * `myGeometry.computeNormals(SMOOTH)`, makes neighboring faces reuse their
     * shared vertices. Surfaces appear smoother with smooth shading. By
     * default, `shadingType` is `FLAT`.
     *
     * The second parameter, `options`, is also optional. If an object with a
     * `roundToPrecision` property is passed, as in
     * `myGeometry.computeNormals(SMOOTH, { roundToPrecision: 5 })`, it sets the
     * number of decimal places to use for calculations. By default,
     * `roundToPrecision` uses 3 decimal places.
     *
     * @param {(FLAT|SMOOTH)} [shadingType=FLAT] shading type. either FLAT or SMOOTH. Defaults to `FLAT`.
     * @param {Object} [options] shading options.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let myGeometry;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Geometry object.
     *   myGeometry = buildGeometry(function() {
     *     torus();
     *   });
     *
     *   // Compute the vertex normals.
     *   myGeometry.computeNormals();
     *
     *   describe(
     *     "A white torus drawn on a dark gray background. Red lines extend outward from the torus' vertices."
     *   );
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Rotate the coordinate system.
     *   rotateX(1);
     *
     *   // Style the helix.
     *   stroke(0);
     *
     *   // Display the helix.
     *   model(myGeometry);
     *
     *   // Style the normal vectors.
     *   stroke(255, 0, 0);
     *
     *   // Iterate over the vertices and vertexNormals arrays.
     *   for (let i = 0; i < myGeometry.vertices.length; i += 1) {
     *
     *     // Get the vertex p5.Vector object.
     *     let v = myGeometry.vertices[i];
     *
     *     // Get the vertex normal p5.Vector object.
     *     let n = myGeometry.vertexNormals[i];
     *
     *     // Calculate a point along the vertex normal.
     *     let p = p5.Vector.mult(n, 5);
     *
     *     // Draw the vertex normal as a red line.
     *     push();
     *     translate(v);
     *     line(0, 0, 0, p.x, p.y, p.z);
     *     pop();
     *   }
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let myGeometry;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Geometry object using a callback function.
     *   myGeometry = new p5.Geometry();
     *
     *   // Create p5.Vector objects to position the vertices.
     *   let v0 = createVector(-40, 0, 0);
     *   let v1 = createVector(0, -40, 0);
     *   let v2 = createVector(0, 40, 0);
     *   let v3 = createVector(40, 0, 0);
     *
     *   // Add the vertices to the p5.Geometry object's vertices array.
     *   myGeometry.vertices.push(v0, v1, v2, v3);
     *
     *   // Compute the faces array.
     *   myGeometry.computeFaces();
     *
     *   // Compute the surface normals.
     *   myGeometry.computeNormals();
     *
     *   describe('A red square drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Add a white point light.
     *   pointLight(255, 255, 255, 0, 0, 10);
     *
     *   // Style the p5.Geometry object.
     *   noStroke();
     *   fill(255, 0, 0);
     *
     *   // Draw the p5.Geometry object.
     *   model(myGeometry);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let myGeometry;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Geometry object.
     *   myGeometry = buildGeometry(createShape);
     *
     *   // Compute normals using default (FLAT) shading.
     *   myGeometry.computeNormals(FLAT);
     *
     *   describe('A white, helical structure drawn on a dark gray background. Its faces appear faceted.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Rotate the coordinate system.
     *   rotateX(1);
     *
     *   // Style the helix.
     *   noStroke();
     *
     *   // Display the helix.
     *   model(myGeometry);
     * }
     *
     * function createShape() {
     *   // Create a helical shape.
     *   beginShape();
     *   for (let i = 0; i < TWO_PI * 3; i += 0.5) {
     *     let x = 30 * cos(i);
     *     let y = 30 * sin(i);
     *     let z = map(i, 0, TWO_PI * 3, -40, 40);
     *     vertex(x, y, z);
     *   }
     *   endShape();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let myGeometry;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Geometry object.
     *   myGeometry = buildGeometry(createShape);
     *
     *   // Compute normals using smooth shading.
     *   myGeometry.computeNormals(SMOOTH);
     *
     *   describe('A white, helical structure drawn on a dark gray background.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Rotate the coordinate system.
     *   rotateX(1);
     *
     *   // Style the helix.
     *   noStroke();
     *
     *   // Display the helix.
     *   model(myGeometry);
     * }
     *
     * function createShape() {
     *   // Create a helical shape.
     *   beginShape();
     *   for (let i = 0; i < TWO_PI * 3; i += 0.5) {
     *     let x = 30 * cos(i);
     *     let y = 30 * sin(i);
     *     let z = map(i, 0, TWO_PI * 3, -40, 40);
     *     vertex(x, y, z);
     *   }
     *   endShape();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let myGeometry;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Geometry object.
     *   myGeometry = buildGeometry(createShape);
     *
     *   // Create an options object.
     *   let options = { roundToPrecision: 5 };
     *
     *   // Compute normals using smooth shading.
     *   myGeometry.computeNormals(SMOOTH, options);
     *
     *   describe('A white, helical structure drawn on a dark gray background.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Rotate the coordinate system.
     *   rotateX(1);
     *
     *   // Style the helix.
     *   noStroke();
     *
     *   // Display the helix.
     *   model(myGeometry);
     * }
     *
     * function createShape() {
     *   // Create a helical shape.
     *   beginShape();
     *   for (let i = 0; i < TWO_PI * 3; i += 0.5) {
     *     let x = 30 * cos(i);
     *     let y = 30 * sin(i);
     *     let z = map(i, 0, TWO_PI * 3, -40, 40);
     *     vertex(x, y, z);
     *   }
     *   endShape();
     * }
     */
    computeNormals(shadingType = FLAT, { roundToPrecision = 3 } = {}) {
      const vertexNormals = this.vertexNormals;
      let vertices = this.vertices;
      const faces = this.faces;
      let iv;

      if (shadingType === SMOOTH) {
        const vertexIndices = {};
        const uniqueVertices = [];

        const power = Math.pow(10, roundToPrecision);
        const rounded = val => Math.round(val * power) / power;
        const getKey = vert =>
          `${rounded(vert.x)},${rounded(vert.y)},${rounded(vert.z)}`;

        // loop through each vertex and add uniqueVertices
        for (let i = 0; i < vertices.length; i++) {
          const vertex = vertices[i];
          const key = getKey(vertex);
          if (vertexIndices[key] === undefined) {
            vertexIndices[key] = uniqueVertices.length;
            uniqueVertices.push(vertex);
          }
        }

        // update face indices to use the deduplicated vertex indices
        faces.forEach(face => {
          for (let fv = 0; fv < 3; ++fv) {
            const originalVertexIndex = face[fv];
            const originalVertex = vertices[originalVertexIndex];
            const key = getKey(originalVertex);
            face[fv] = vertexIndices[key];
          }
        });

        // update edge indices to use the deduplicated vertex indices
        this.edges.forEach(edge => {
          for (let ev = 0; ev < 2; ++ev) {
            const originalVertexIndex = edge[ev];
            const originalVertex = vertices[originalVertexIndex];
            const key = getKey(originalVertex);
            edge[ev] = vertexIndices[key];
          }
        });

        // update the deduplicated vertices
        this.vertices = vertices = uniqueVertices;
      }

      // initialize the vertexNormals array with empty vectors
      vertexNormals.length = 0;
      for (iv = 0; iv < vertices.length; ++iv) {
        vertexNormals.push(new Vector());
      }

      // loop through all the faces adding its normal to the normal
      // of each of its vertices
      faces.forEach((face, f) => {
        const faceNormal = this._getFaceNormal(f);

        // all three vertices get the normal added
        for (let fv = 0; fv < 3; ++fv) {
          const vertexIndex = face[fv];
          vertexNormals[vertexIndex].add(faceNormal);
        }
      });

      // normalize the normals
      for (iv = 0; iv < vertices.length; ++iv) {
        vertexNormals[iv].normalize();
      }

      return this;
    }

    /**
     * Averages the vertex normals. Used in curved
     * surfaces
     * @private
     * @chainable
     */
    averageNormals() {
      for (let i = 0; i <= this.detailY; i++) {
        const offset = this.detailX + 1;
        let temp = Vector.add(
          this.vertexNormals[i * offset],
          this.vertexNormals[i * offset + this.detailX]
        );

        temp = Vector.div(temp, 2);
        this.vertexNormals[i * offset] = temp;
        this.vertexNormals[i * offset + this.detailX] = temp;
      }
      return this;
    }

    /**
     * Averages pole normals.  Used in spherical primitives
     * @private
     * @chainable
     */
    averagePoleNormals() {
      //average the north pole
      let sum = new Vector(0, 0, 0);
      for (let i = 0; i < this.detailX; i++) {
        sum.add(this.vertexNormals[i]);
      }
      sum = Vector.div(sum, this.detailX);

      for (let i = 0; i < this.detailX; i++) {
        this.vertexNormals[i] = sum;
      }

      //average the south pole
      sum = new Vector(0, 0, 0);
      for (
        let i = this.vertices.length - 1;
        i > this.vertices.length - 1 - this.detailX;
        i--
      ) {
        sum.add(this.vertexNormals[i]);
      }
      sum = Vector.div(sum, this.detailX);

      for (
        let i = this.vertices.length - 1;
        i > this.vertices.length - 1 - this.detailX;
        i--
      ) {
        this.vertexNormals[i] = sum;
      }
      return this;
    }

    /**
     * Create a 2D array for establishing stroke connections
     * @private
     * @chainable
     */
    _makeTriangleEdges() {
      this.edges.length = 0;

      for (let j = 0; j < this.faces.length; j++) {
        this.edges.push([this.faces[j][0], this.faces[j][1]]);
        this.edges.push([this.faces[j][1], this.faces[j][2]]);
        this.edges.push([this.faces[j][2], this.faces[j][0]]);
      }

      return this;
    }

    /**
     * @example
     * let tetrahedron;
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   describe('A rotating tetrahedron');
     *
     *   tetrahedron = new p5.Geometry();
     *
     *   // Give each geometry a unique gid
     *   tetrahedron.gid = 'tetrahedron';
     *
     *   // Add four points of the tetrahedron
     *
     *   let radius = 50;
     *   // A 2D triangle:
     *   tetrahedron.vertices.push(createVector(radius, 0, 0));
     *   tetrahedron.vertices.push(createVector(radius, 0, 0).rotate(TWO_PI / 3));
     *   tetrahedron.vertices.push(createVector(radius, 0, 0).rotate(TWO_PI * 2 / 3));
     *   // Add a tip in the z axis:
     *   tetrahedron.vertices.push(createVector(0, 0, radius));
     *
     *   // Create the four faces by connecting the sets of three points
     *   tetrahedron.faces.push([0, 1, 2]);
     *   tetrahedron.faces.push([0, 1, 3]);
     *   tetrahedron.faces.push([0, 2, 3]);
     *   tetrahedron.faces.push([1, 2, 3]);
     *   tetrahedron.makeEdgesFromFaces();
     * }
     *
     * function draw() {
     *   background(200);
     *   strokeWeight(2);
     *   orbitControl();
     *   rotateY(millis() * 0.001);
     *   model(tetrahedron);
     * }
     */
    makeEdgesFromFaces() {
      this._makeTriangleEdges();
    }

    /**
     * Converts each line segment into the vertices and vertex attributes needed
     * to turn the line into a polygon on screen. This will include:
     * - Two triangles line segment to create a rectangle
     * - Two triangles per endpoint to create a stroke cap rectangle. A fragment
     *   shader is responsible for displaying the appropriate cap style within
     *   that rectangle.
     * - Four triangles per join between adjacent line segments, creating a quad on
     *   either side of the join, perpendicular to the lines. A vertex shader will
     *   discard the quad in the "elbow" of the join, and a fragment shader will
     *   display the appropriate join style within the remaining quad.
     *
     * @private
     * @chainable
     */
    _edgesToVertices() {
      this.lineVertices.clear();
      this.lineTangentsIn.clear();
      this.lineTangentsOut.clear();
      this.lineSides.clear();

      const potentialCaps = new Map();
      const connected = new Set();
      let lastValidDir;
      for (let i = 0; i < this.edges.length; i++) {
        const prevEdge = this.edges[i - 1];
        const currEdge = this.edges[i];
        const isPoint = currEdge[0] === currEdge[1];
        const begin = this.vertices[currEdge[0]];
        const end = this.vertices[currEdge[1]];
        const prevColor = (this.vertexStrokeColors.length > 0 && prevEdge)
          ? this.vertexStrokeColors.slice(
            prevEdge[1] * 4,
            (prevEdge[1] + 1) * 4
          )
          : [0, 0, 0, 0];
        const fromColor = this.vertexStrokeColors.length > 0
          ? this.vertexStrokeColors.slice(
            currEdge[0] * 4,
            (currEdge[0] + 1) * 4
          )
          : [0, 0, 0, 0];
        const toColor = this.vertexStrokeColors.length > 0
          ? this.vertexStrokeColors.slice(
            currEdge[1] * 4,
            (currEdge[1] + 1) * 4
          )
          : [0, 0, 0, 0];
        const dir = isPoint
          ? new Vector(0, 1, 0)
          : end
            .copy()
            .sub(begin)
            .normalize();
        const dirOK = dir.magSq() > 0;
        if (dirOK) {
          this._addSegment(begin, end, fromColor, toColor, dir);
        }
        if (!this.renderer?._simpleLines) {
          if (i > 0 && prevEdge[1] === currEdge[0]) {
            if (!connected.has(currEdge[0])) {
              connected.add(currEdge[0]);
              potentialCaps.delete(currEdge[0]);
              // Add a join if this segment shares a vertex with the previous. Skip
              // actually adding join vertices if either the previous segment or this
              // one has a length of 0.
              //
              // Don't add a join if the tangents point in the same direction, which
              // would mean the edges line up exactly, and there is no need for a join.
              if (lastValidDir && dirOK && dir.dot(lastValidDir) < 1 - 1e-8) {
                this._addJoin(begin, lastValidDir, dir, fromColor);
              }
            }
          } else if (isPoint) {
            this._addCap(begin, dir.copy().mult(-1), fromColor);
            this._addCap(begin, dir, fromColor);
          } else {
            // Start a new line
            if (dirOK && !connected.has(currEdge[0])) {
              const existingCap = potentialCaps.get(currEdge[0]);
              if (existingCap) {
                this._addJoin(
                  begin,
                  existingCap.dir,
                  dir,
                  fromColor
                );
                potentialCaps.delete(currEdge[0]);
                connected.add(currEdge[0]);
              } else {
                potentialCaps.set(currEdge[0], {
                  point: begin,
                  dir: dir.copy().mult(-1),
                  color: fromColor
                });
              }
            }
            if (!isPoint && lastValidDir && !connected.has(prevEdge[1])) {
              const existingCap = potentialCaps.get(prevEdge[1]);
              if (existingCap) {
                this._addJoin(
                  this.vertices[prevEdge[1]],
                  lastValidDir,
                  existingCap.dir.copy().mult(-1),
                  prevColor
                );
                potentialCaps.delete(prevEdge[1]);
                connected.add(prevEdge[1]);
              } else {
                // Close off the last segment with a cap
                potentialCaps.set(prevEdge[1], {
                  point: this.vertices[prevEdge[1]],
                  dir: lastValidDir,
                  color: prevColor
                });
              }
              lastValidDir = undefined;
            }
          }

          if (i === this.edges.length - 1 && !connected.has(currEdge[1])) {
            const existingCap = potentialCaps.get(currEdge[1]);
            if (existingCap) {
              this._addJoin(
                end,
                dir,
                existingCap.dir.copy().mult(-1),
                toColor
              );
              potentialCaps.delete(currEdge[1]);
              connected.add(currEdge[1]);
            } else {
              potentialCaps.set(currEdge[1], {
                point: end,
                dir,
                color: toColor
              });
            }
          }

          if (dirOK) {
            lastValidDir = dir;
          }
        }
      }
      for (const { point, dir, color } of potentialCaps.values()) {
        this._addCap(point, dir, color);
      }
      return this;
    }

    /**
     * Adds the vertices and vertex attributes for two triangles making a rectangle
     * for a straight line segment. A vertex shader is responsible for picking
     * proper coordinates on the screen given the centerline positions, the tangent,
     * and the side of the centerline each vertex belongs to. Sides follow the
     * following scheme:
     *
     *  -1            -1
     *   o-------------o
     *   |             |
     *   o-------------o
     *   1             1
     *
     * @private
     * @chainable
     */
    _addSegment(
      begin,
      end,
      fromColor,
      toColor,
      dir
    ) {
      const a = begin.array();
      const b = end.array();
      const dirArr = dir.array();
      this.lineSides.push(1, 1, -1, 1, -1, -1);
      for (const tangents of [this.lineTangentsIn, this.lineTangentsOut]) {
        for (let i = 0; i < 6; i++) {
          tangents.push(...dirArr);
        }
      }
      this.lineVertices.push(...a, ...b, ...a, ...b, ...b, ...a);
      if (!this.renderer?._simpleLines) {
        this.lineVertexColors.push(
          ...fromColor,
          ...toColor,
          ...fromColor,
          ...toColor,
          ...toColor,
          ...fromColor
        );
      }
      return this;
    }

    /**
     * Adds the vertices and vertex attributes for two triangles representing the
     * stroke cap of a line. A fragment shader is responsible for displaying the
     * appropriate cap style within the rectangle they make.
     *
     * The lineSides buffer will include the following values for the points on
     * the cap rectangle:
     *
     *           -1  -2
     * -----------o---o
     *            |   |
     * -----------o---o
     *            1   2
     * @private
     * @chainable
     */
    _addCap(point, tangent, color) {
      const ptArray = point.array();
      const tanInArray = tangent.array();
      const tanOutArray = [0, 0, 0];
      for (let i = 0; i < 6; i++) {
        this.lineVertices.push(...ptArray);
        this.lineTangentsIn.push(...tanInArray);
        this.lineTangentsOut.push(...tanOutArray);
        this.lineVertexColors.push(...color);
      }
      this.lineSides.push(-1, 2, -2, 1, 2, -1);
      return this;
    }

    /**
     * Adds the vertices and vertex attributes for four triangles representing a
     * join between two adjacent line segments. This creates a quad on either side
     * of the shared vertex of the two line segments, with each quad perpendicular
     * to the lines. A vertex shader will discard all but the quad in the "elbow" of
     * the join, and a fragment shader will display the appropriate join style
     * within the remaining quad.
     *
     * The lineSides buffer will include the following values for the points on
     * the join rectangles:
     *
     *            -1     -2
     * -------------o----o
     *              |    |
     *       1 o----o----o -3
     *         |    | 0  |
     * --------o----o    |
     *        2|    3    |
     *         |         |
     *         |         |
     * @private
     * @chainable
     */
    _addJoin(
      point,
      fromTangent,
      toTangent,
      color
    ) {
      const ptArray = point.array();
      const tanInArray = fromTangent.array();
      const tanOutArray = toTangent.array();
      for (let i = 0; i < 12; i++) {
        this.lineVertices.push(...ptArray);
        this.lineTangentsIn.push(...tanInArray);
        this.lineTangentsOut.push(...tanOutArray);
        this.lineVertexColors.push(...color);
      }
      this.lineSides.push(-1, -3, -2, -1, 0, -3);
      this.lineSides.push(3, 1, 2, 3, 0, 1);
      return this;
    }

    /**
     * Transforms the geometry's vertices to fit snugly within a 100×100×100 box
     * centered at the origin.
     *
     * Calling `myGeometry.normalize()` translates the geometry's vertices so that
     * they're centered at the origin `(0, 0, 0)`. Then it scales the vertices so
     * that they fill a 100×100×100 box. As a result, small geometries will grow
     * and large geometries will shrink.
     *
     * Note: `myGeometry.normalize()` only works when called in the
     * <a href="#/p5/setup">setup()</a> function.
     *
     * @chainable
     *
     * @example
     * let myGeometry;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a very small torus.
     *   myGeometry = buildGeometry(function() {;
     *     torus(1, 0.25);
     *   });
     *
     *   // Normalize the torus so its vertices fill
     *   // the range [-100, 100].
     *   myGeometry.normalize();
     *
     *   describe('A white torus rotates slowly against a dark gray background.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Rotate around the y-axis.
     *   rotateY(frameCount * 0.01);
     *
     *   // Style the torus.
     *   noStroke();
     *
     *   // Draw the torus.
     *   model(myGeometry);
     * }
     */
    normalize() {
      if (this.vertices.length > 0) {
        // Find the corners of our bounding box
        const maxPosition = this.vertices[0].copy();
        const minPosition = this.vertices[0].copy();

        for (let i = 0; i < this.vertices.length; i++) {
          maxPosition.x = Math.max(maxPosition.x, this.vertices[i].x);
          minPosition.x = Math.min(minPosition.x, this.vertices[i].x);
          maxPosition.y = Math.max(maxPosition.y, this.vertices[i].y);
          minPosition.y = Math.min(minPosition.y, this.vertices[i].y);
          maxPosition.z = Math.max(maxPosition.z, this.vertices[i].z);
          minPosition.z = Math.min(minPosition.z, this.vertices[i].z);
        }

        const center = Vector.lerp(maxPosition, minPosition, 0.5);
        const dist = Vector.sub(maxPosition, minPosition);
        const longestDist = Math.max(Math.max(dist.x, dist.y), dist.z);
        const scale = 200 / longestDist;

        for (let i = 0; i < this.vertices.length; i++) {
          this.vertices[i].sub(center);
          this.vertices[i].mult(scale);
        }
      }
      return this;
    }

    /** Sets the shader's vertex property or attribute variables.
     *
     * A vertex property, or vertex attribute, is a variable belonging to a vertex in a shader. p5.js provides some
     * default properties, such as `aPosition`, `aNormal`, `aVertexColor`, etc. These are
     * set using <a href="#/p5/vertex">vertex()</a>, <a href="#/p5/normal">normal()</a>
     * and <a href="#/p5/fill">fill()</a> respectively. Custom properties can also
     * be defined within <a href="#/p5/beginShape">beginShape()</a> and
     * <a href="#/p5/endShape">endShape()</a>.
     *
     * The first parameter, `propertyName`, is a string with the property's name.
     * This is the same variable name which should be declared in the shader, as in
     * `in vec3 aProperty`, similar to .`setUniform()`.
     *
     * The second parameter, `data`, is the value assigned to the shader variable. This value
     * will be pushed directly onto the Geometry object. There should be the same number
     * of custom property values as vertices, this method should be invoked once for each
     * vertex.
     *
     * The `data` can be a Number or an array of numbers. Tn the shader program the type
     * can be declared according to the WebGL specification. Common types include `float`,
     * `vec2`, `vec3`, `vec4` or matrices.
     *
     * See also the global <a href="#/p5/vertexProperty">vertexProperty()</a> function.
     *
     * @example
     * let geo;
     *
     * function cartesianToSpherical(x, y, z) {
     *   let r = sqrt(pow(x, 2) + pow(y, 2) + pow(z, 2));
     *   let theta = acos(z / r);
     *   let phi = atan2(y, x);
     *   return { theta, phi };
     * }
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Modify the material shader to display roughness.
     *   const myShader = baseMaterialShader().modify({
     *     vertexDeclarations:`in float aRoughness;
     *                         out float vRoughness;`,
     *     fragmentDeclarations: 'in float vRoughness;',
     *     'void afterVertex': `() {
     *         vRoughness = aRoughness;
     *     }`,
     *     'vec4 combineColors': `(ColorComponents components) {
     *             vec4 color = vec4(0.);
     *             color.rgb += components.diffuse * components.baseColor * (1.0-vRoughness);
     *             color.rgb += components.ambient * components.ambientColor;
     *             color.rgb += components.specular * components.specularColor * (1.0-vRoughness);
     *             color.a = components.opacity;
     *             return color;
     *     }`
     *   });
     *
     *   // Create the Geometry object.
     *   geo = buildGeometry(function() {
     *     fill('hotpink');
     *     sphere(45, 50, 50);
     *   });
     *
     *   // Set the roughness value for every vertex.
     *   for (let v of geo.vertices){
     *
     *     // convert coordinates to spherical coordinates
     *     let spherical = cartesianToSpherical(v.x, v.y, v.z);
     *
     *     // Set the custom roughness vertex property.
     *     let roughness = noise(spherical.theta*5, spherical.phi*5);
     *     geo.vertexProperty('aRoughness', roughness);
     *   }
     *
     *   // Use the custom shader.
     *   shader(myShader);
     *
     *   describe('A rough pink sphere rotating on a blue background.');
     * }
     *
     * function draw() {
     *   // Set some styles and lighting
     *   background('lightblue');
     *   noStroke();
     *
     *   specularMaterial(255,125,100);
     *   shininess(2);
     *
     *   directionalLight('white', -1, 1, -1);
     *   ambientLight(320);
     *
     *   rotateY(millis()*0.001);
     *
     *   // Draw the geometry
     *   model(geo);
     * }
     *
     * @param {String} propertyName the name of the vertex property.
     * @param {Number|Number[]} data the data tied to the vertex property.
     * @param {Number} [size] optional size of each unit of data.
     */
    vertexProperty(propertyName, data, size){
      let prop;
      if (!this.userVertexProperties[propertyName]){
        prop = this.userVertexProperties[propertyName] =
          this._userVertexPropertyHelper(propertyName, data, size);
      }
      prop = this.userVertexProperties[propertyName];
      if (size){
        prop.pushDirect(data);
      } else {
        prop.setCurrentData(data);
        prop.pushCurrentData();
      }
    }

    _userVertexPropertyHelper(propertyName, data, size){
      const geometryInstance = this;
      const prop = this.userVertexProperties[propertyName] = {
        name: propertyName,
        dataSize: size ? size : data.length ? data.length : 1,
        geometry: geometryInstance,
        // Getters
        getName(){
          return this.name;
        },
        getCurrentData(){
          if (this.currentData === undefined) {
            this.currentData = new Array(this.getDataSize()).fill(0);
          }
          return this.currentData;
        },
        getDataSize() {
          return this.dataSize;
        },
        getSrcName() {
          const src = this.name.concat('Src');
          return src;
        },
        getDstName() {
          const dst = this.name.concat('Buffer');
          return dst;
        },
        getSrcArray() {
          const srcName = this.getSrcName();
          return this.geometry[srcName];
        },
        //Setters
        setCurrentData(data) {
          // if (size != this.getDataSize()){
          //   p5._friendlyError(`Custom vertex property '${this.name}' has been set with various data sizes. You can change it's name, or if it was an accident, set '${this.name}' to have the same number of inputs each time!`, 'vertexProperty()');
          // }
          this.currentData = data;
        },
        // Utilities
        pushCurrentData(){
          const data = this.getCurrentData();
          this.pushDirect(data);
        },
        pushDirect(data) {
          if (data.length){
            this.getSrcArray().push(...data);
          } else {
            this.getSrcArray().push(data);
          }
        },
        resetSrcArray(){
          this.geometry[this.getSrcName()] = [];
        },
        delete() {
          const srcName = this.getSrcName();
          delete this.geometry[srcName];
          delete this;
        }
      };
      this[prop.getSrcName()] = [];
      return this.userVertexProperties[propertyName];
    }
  }
  /**
   * Keeps track of how many custom geometry objects have been made so that each
   * can be assigned a unique ID.
   */
  Geometry.nextId = 0;

  function geometry(p5, fn){
    /**
     * A class to describe a 3D shape.
     *
     * Each `p5.Geometry` object represents a 3D shape as a set of connected
     * points called *vertices*. All 3D shapes are made by connecting vertices to
     * form triangles that are stitched together. Each triangular patch on the
     * geometry's surface is called a *face*. The geometry stores information
     * about its vertices and faces for use with effects such as lighting and
     * texture mapping.
     *
     * The first parameter, `detailX`, is optional. If a number is passed, as in
     * `new p5.Geometry(24)`, it sets the number of triangle subdivisions to use
     * along the geometry's x-axis. By default, `detailX` is 1.
     *
     * The second parameter, `detailY`, is also optional. If a number is passed,
     * as in `new p5.Geometry(24, 16)`, it sets the number of triangle
     * subdivisions to use along the geometry's y-axis. By default, `detailX` is
     * 1.
     *
     * The third parameter, `callback`, is also optional. If a function is passed,
     * as in `new p5.Geometry(24, 16, createShape)`, it will be called once to add
     * vertices to the new 3D shape.
     *
     * @class p5.Geometry
     * @param  {Integer} [detailX] number of vertices along the x-axis.
     * @param  {Integer} [detailY] number of vertices along the y-axis.
     * @param {Function} [callback] function to call once the geometry is created.
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let myGeometry;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Geometry object.
     *   myGeometry = new p5.Geometry();
     *
     *   // Create p5.Vector objects to position the vertices.
     *   let v0 = createVector(-40, 0, 0);
     *   let v1 = createVector(0, -40, 0);
     *   let v2 = createVector(40, 0, 0);
     *
     *   // Add the vertices to the p5.Geometry object's vertices array.
     *   myGeometry.vertices.push(v0, v1, v2);
     *
     *   describe('A white triangle drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the p5.Geometry object.
     *   model(myGeometry);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let myGeometry;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Geometry object using a callback function.
     *   myGeometry = new p5.Geometry(1, 1, createShape);
     *
     *   describe('A white triangle drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the p5.Geometry object.
     *   model(myGeometry);
     * }
     *
     * function createShape() {
     *   // Create p5.Vector objects to position the vertices.
     *   let v0 = createVector(-40, 0, 0);
     *   let v1 = createVector(0, -40, 0);
     *   let v2 = createVector(40, 0, 0);
     *
     *   // "this" refers to the p5.Geometry object being created.
     *
     *   // Add the vertices to the p5.Geometry object's vertices array.
     *   this.vertices.push(v0, v1, v2);
     *
     *   // Add an array to list which vertices belong to the face.
     *   // Vertices are listed in clockwise "winding" order from
     *   // left to top to right.
     *   this.faces.push([0, 1, 2]);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let myGeometry;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Geometry object using a callback function.
     *   myGeometry = new p5.Geometry(1, 1, createShape);
     *
     *   describe('A white triangle drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the p5.Geometry object.
     *   model(myGeometry);
     * }
     *
     * function createShape() {
     *   // Create p5.Vector objects to position the vertices.
     *   let v0 = createVector(-40, 0, 0);
     *   let v1 = createVector(0, -40, 0);
     *   let v2 = createVector(40, 0, 0);
     *
     *   // "this" refers to the p5.Geometry object being created.
     *
     *   // Add the vertices to the p5.Geometry object's vertices array.
     *   this.vertices.push(v0, v1, v2);
     *
     *   // Add an array to list which vertices belong to the face.
     *   // Vertices are listed in clockwise "winding" order from
     *   // left to top to right.
     *   this.faces.push([0, 1, 2]);
     *
     *   // Compute the surface normals to help with lighting.
     *   this.computeNormals();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * // Adapted from Paul Wheeler's wonderful p5.Geometry tutorial.
     * // https://www.paulwheeler.us/articles/custom-3d-geometry-in-p5js/
     * // CC-BY-SA 4.0
     *
     * let myGeometry;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the p5.Geometry object.
     *   // Set detailX to 48 and detailY to 2.
     *   // >>> try changing them.
     *   myGeometry = new p5.Geometry(48, 2, createShape);
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Style the p5.Geometry object.
     *   strokeWeight(0.2);
     *
     *   // Draw the p5.Geometry object.
     *   model(myGeometry);
     * }
     *
     * function createShape() {
     *   // "this" refers to the p5.Geometry object being created.
     *
     *   // Define the Möbius strip with a few parameters.
     *   let spread = 0.1;
     *   let radius = 30;
     *   let stripWidth = 15;
     *   let xInterval = 4 * PI / this.detailX;
     *   let yOffset = -stripWidth / 2;
     *   let yInterval = stripWidth / this.detailY;
     *
     *   for (let j = 0; j <= this.detailY; j += 1) {
     *     // Calculate the "vertical" point along the strip.
     *     let v = yOffset + yInterval * j;
     *
     *     for (let i = 0; i <= this.detailX; i += 1) {
     *       // Calculate the angle of rotation around the strip.
     *       let u = i * xInterval;
     *
     *       // Calculate the coordinates of the vertex.
     *       let x = (radius + v * cos(u / 2)) * cos(u) - sin(u / 2) * 2 * spread;
     *       let y = (radius + v * cos(u / 2)) * sin(u);
     *       if (u < TWO_PI) {
     *         y += sin(u) * spread;
     *       } else {
     *         y -= sin(u) * spread;
     *       }
     *       let z = v * sin(u / 2) + sin(u / 4) * 4 * spread;
     *
     *       // Create a p5.Vector object to position the vertex.
     *       let vert = createVector(x, y, z);
     *
     *       // Add the vertex to the p5.Geometry object's vertices array.
     *       this.vertices.push(vert);
     *     }
     *   }
     *
     *   // Compute the faces array.
     *   this.computeFaces();
     *
     *   // Compute the surface normals to help with lighting.
     *   this.computeNormals();
     * }
     */
    p5.Geometry = Geometry;

    /**
     * An array with the geometry's vertices.
     *
     * The geometry's vertices are stored as
     * <a href="#/p5.Vector">p5.Vector</a> objects in the `myGeometry.vertices`
     * array. The geometry's first vertex is the
     * <a href="#/p5.Vector">p5.Vector</a> object at `myGeometry.vertices[0]`,
     * its second vertex is `myGeometry.vertices[1]`, its third vertex is
     * `myGeometry.vertices[2]`, and so on.
     *
     * @property vertices
     * @for p5.Geometry
     * @name vertices
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let myGeometry;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Geometry object.
     *   myGeometry = new p5.Geometry();
     *
     *   // Create p5.Vector objects to position the vertices.
     *   let v0 = createVector(-40, 0, 0);
     *   let v1 = createVector(0, -40, 0);
     *   let v2 = createVector(40, 0, 0);
     *
     *   // Add the vertices to the p5.Geometry object's vertices array.
     *   myGeometry.vertices.push(v0, v1, v2);
     *
     *   describe('A white triangle drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the p5.Geometry object.
     *   model(myGeometry);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let myGeometry;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Geometry object.
     *   myGeometry = buildGeometry(function() {
     *     torus(30, 15, 10, 8);
     *   });
     *
     *   describe('A white torus rotates slowly against a dark gray background. Red spheres mark its vertices.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Rotate the coordinate system.
     *   rotateY(frameCount * 0.01);
     *
     *   // Style the p5.Geometry object.
     *   fill(255);
     *   stroke(0);
     *
     *   // Display the p5.Geometry object.
     *   model(myGeometry);
     *
     *   // Style the vertices.
     *   fill(255, 0, 0);
     *   noStroke();
     *
     *   // Iterate over the vertices array.
     *   for (let v of myGeometry.vertices) {
     *     // Draw a sphere to mark the vertex.
     *     push();
     *     translate(v);
     *     sphere(2.5);
     *     pop();
     *   }
     * }
     */

    /**
     * An array with the vectors that are normal to the geometry's vertices.
     *
     * A face's orientation is defined by its *normal vector* which points out
     * of the face and is normal (perpendicular) to the surface. Calling
     * `myGeometry.computeNormals()` first calculates each face's normal
     * vector. Then it calculates the normal vector for each vertex by
     * averaging the normal vectors of the faces surrounding the vertex. The
     * vertex normals are stored as <a href="#/p5.Vector">p5.Vector</a>
     * objects in the `myGeometry.vertexNormals` array.
     *
     * @property vertexNormals
     * @name vertexNormals
     * @for p5.Geometry
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let myGeometry;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Geometry object.
     *   myGeometry = buildGeometry(function() {
     *     torus(30, 15, 10, 8);
     *   });
     *
     *   // Compute the vertex normals.
     *   myGeometry.computeNormals();
     *
     *   describe(
     *     'A white torus rotates against a dark gray background. Red lines extend outward from its vertices.'
     *   );
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Rotate the coordinate system.
     *   rotateY(frameCount * 0.01);
     *
     *   // Style the p5.Geometry object.
     *   stroke(0);
     *
     *   // Display the p5.Geometry object.
     *   model(myGeometry);
     *
     *   // Style the normal vectors.
     *   stroke(255, 0, 0);
     *
     *   // Iterate over the vertices and vertexNormals arrays.
     *   for (let i = 0; i < myGeometry.vertices.length; i += 1) {
     *
     *     // Get the vertex p5.Vector object.
     *     let v = myGeometry.vertices[i];
     *
     *     // Get the vertex normal p5.Vector object.
     *     let n = myGeometry.vertexNormals[i];
     *
     *     // Calculate a point along the vertex normal.
     *     let p = p5.Vector.mult(n, 8);
     *
     *     // Draw the vertex normal as a red line.
     *     push();
     *     translate(v);
     *     line(0, 0, 0, p.x, p.y, p.z);
     *     pop();
     *   }
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let myGeometry;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Geometry object.
     *   myGeometry = new p5.Geometry();
     *
     *   // Create p5.Vector objects to position the vertices.
     *   let v0 = createVector(-40, 0, 0);
     *   let v1 = createVector(0, -40, 0);
     *   let v2 = createVector(0, 40, 0);
     *   let v3 = createVector(40, 0, 0);
     *
     *   // Add the vertices to the p5.Geometry object's vertices array.
     *   myGeometry.vertices.push(v0, v1, v2, v3);
     *
     *   // Compute the faces array.
     *   myGeometry.computeFaces();
     *
     *   // Compute the surface normals.
     *   myGeometry.computeNormals();
     *
     *   describe('A red square drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Add a white point light.
     *   pointLight(255, 255, 255, 0, 0, 10);
     *
     *   // Style the p5.Geometry object.
     *   noStroke();
     *   fill(255, 0, 0);
     *
     *   // Display the p5.Geometry object.
     *   model(myGeometry);
     * }
     */

    /**
     * An array that lists which of the geometry's vertices form each of its
     * faces.
     *
     * All 3D shapes are made by connecting sets of points called *vertices*. A
     * geometry's surface is formed by connecting vertices to form triangles
     * that are stitched together. Each triangular patch on the geometry's
     * surface is called a *face*.
     *
     * The geometry's vertices are stored as
     * <a href="#/p5.Vector">p5.Vector</a> objects in the
     * <a href="#/p5.Geometry/vertices">myGeometry.vertices</a> array. The
     * geometry's first vertex is the <a href="#/p5.Vector">p5.Vector</a>
     * object at `myGeometry.vertices[0]`, its second vertex is
     * `myGeometry.vertices[1]`, its third vertex is `myGeometry.vertices[2]`,
     * and so on.
     *
     * For example, a geometry made from a rectangle has two faces because a
     * rectangle is made by joining two triangles. `myGeometry.faces` for a
     * rectangle would be the two-dimensional array `[[0, 1, 2], [2, 1, 3]]`.
     * The first face, `myGeometry.faces[0]`, is the array `[0, 1, 2]` because
     * it's formed by connecting `myGeometry.vertices[0]`,
     * `myGeometry.vertices[1]`,and `myGeometry.vertices[2]`. The second face,
     * `myGeometry.faces[1]`, is the array `[2, 1, 3]` because it's formed by
     * connecting `myGeometry.vertices[2]`, `myGeometry.vertices[1]`,and
     * `myGeometry.vertices[3]`.
     *
     * @property faces
     * @name faces
     * @for p5.Geometry
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let myGeometry;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Geometry object.
     *   myGeometry = buildGeometry(function() {
     *     sphere();
     *   });
     *
     *   describe("A sphere drawn on a gray background. The sphere's surface is a grayscale patchwork of triangles.");
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Style the p5.Geometry object.
     *   noStroke();
     *
     *   // Set a random seed.
     *   randomSeed(1234);
     *
     *   // Iterate over the faces array.
     *   for (let face of myGeometry.faces) {
     *
     *     // Style the face.
     *     let g = random(0, 255);
     *     fill(g);
     *
     *     // Draw the face.
     *     beginShape();
     *     // Iterate over the vertices that form the face.
     *     for (let f of face) {
     *       // Get the vertex's p5.Vector object.
     *       let v = myGeometry.vertices[f];
     *       vertex(v.x, v.y, v.z);
     *     }
     *     endShape();
     *
     *   }
     * }
     */

    /**
     * An array that lists the texture coordinates for each of the geometry's
     * vertices.
     *
     * In order for <a href="#/p5/texture">texture()</a> to work, the geometry
     * needs a way to map the points on its surface to the pixels in a
     * rectangular image that's used as a texture. The geometry's vertex at
     * coordinates `(x, y, z)` maps to the texture image's pixel at coordinates
     * `(u, v)`.
     *
     * The `myGeometry.uvs` array stores the `(u, v)` coordinates for each
     * vertex in the order it was added to the geometry. For example, the
     * first vertex, `myGeometry.vertices[0]`, has its `(u, v)` coordinates
     * stored at `myGeometry.uvs[0]` and `myGeometry.uvs[1]`.
     *
     * @property uvs
     * @name uvs
     * @for p5.Geometry
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   img = await loadImage('assets/laDefense.jpg');
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   // Create p5.Geometry objects.
     *   let geom1 = buildGeometry(createShape);
     *   let geom2 = buildGeometry(createShape);
     *
     *   // Left (original).
     *   push();
     *   translate(-25, 0, 0);
     *   texture(img);
     *   noStroke();
     *   model(geom1);
     *   pop();
     *
     *   // Set geom2's texture coordinates.
     *   geom2.uvs = [0.25, 0.25, 0.75, 0.25, 0.25, 0.75, 0.75, 0.75];
     *
     *   // Right (zoomed in).
     *   push();
     *   translate(25, 0, 0);
     *   texture(img);
     *   noStroke();
     *   model(geom2);
     *   pop();
     *
     *   describe(
     *     'Two photos of a ceiling on a gray background. The photo on the right zooms in to the center of the photo.'
     *   );
     * }
     *
     * function createShape() {
     *   plane(40);
     * }
     */

    /**
     * A unique identifier for this geometry. The renderer will use this to cache resources.
     *
     * @property {String} gid
     * @for p5.Geometry
     */
  }

  if(typeof p5 !== 'undefined'){
    geometry(p5);
  }

  /**
   * @private
   * A class responsible for converting successive WebGL draw calls into a single
   * `p5.Geometry` that can be reused and drawn with `model()`.
   */
  class GeometryBuilder {
    constructor(renderer) {
      this.renderer = renderer;
      renderer._pInst.push();
      this.identityMatrix = new Matrix(4);
      renderer.states.setValue('uModelMatrix', new Matrix(4));
      this.geometry = new Geometry(
        undefined,
        undefined,
        undefined,
        this.renderer
      );
      this.geometry.gid = `_p5_GeometryBuilder_${GeometryBuilder.nextGeometryId}`;
      GeometryBuilder.nextGeometryId++;
      this.hasTransform = false;
    }

    /**
     * @private
     * Applies the current transformation matrix to each vertex.
     */
    transformVertices(vertices) {
      if (!this.hasTransform) return vertices;

      return vertices.map(v =>
        this.renderer.states.uModelMatrix.multiplyPoint(v)
      );
    }

    /**
     * @private
     * Applies the current normal matrix to each normal.
     */
    transformNormals(normals) {
      if (!this.hasTransform) return normals;

      return normals.map(
        v => this.renderer.scratchMat3.multiplyVec(v) // this is a vec3
      );
    }

    /**
     * @private
     * Adds a p5.Geometry to the builder's combined geometry, flattening
     * transformations.
     */
    addGeometry(input) {
      this.hasTransform = !this.renderer.states.uModelMatrix.mat4
        .every((v, i) => v === this.identityMatrix.mat4[i]);

      if (this.hasTransform) {
        this.renderer.scratchMat3.inverseTranspose4x4(
          this.renderer.states.uModelMatrix
        );
      }

      let startIdx = this.geometry.vertices.length;
      this.geometry.vertices.push(...this.transformVertices(input.vertices));
      this.geometry.vertexNormals.push(
        ...this.transformNormals(input.vertexNormals)
      );
      this.geometry.uvs.push(...input.uvs);

      const inputUserVertexProps = input.userVertexProperties;
      const builtUserVertexProps = this.geometry.userVertexProperties;
      const numPreviousVertices =
        this.geometry.vertices.length - input.vertices.length;

      for (const propName in builtUserVertexProps){
        if (propName in inputUserVertexProps){
          continue;
        }
        const prop = builtUserVertexProps[propName];
        const size = prop.getDataSize();
        const numMissingValues = size * input.vertices.length;
        const missingValues = Array(numMissingValues).fill(0);
        prop.pushDirect(missingValues);
      }
      for (const propName in inputUserVertexProps){
        const prop = inputUserVertexProps[propName];
        const data = prop.getSrcArray();
        const size = prop.getDataSize();
        if (numPreviousVertices > 0 && !(propName in builtUserVertexProps)){
          const numMissingValues = size * numPreviousVertices;
          const missingValues = Array(numMissingValues).fill(0);
          this.geometry.vertexProperty(propName, missingValues, size);
        }
        this.geometry.vertexProperty(propName, data, size);
      }

      if (this.renderer.states.fillColor) {
        this.geometry.faces.push(
          ...input.faces.map(f => f.map(idx => idx + startIdx))
        );
      }
      if (this.renderer.states.strokeColor) {
        this.geometry.edges.push(
          ...input.edges.map(edge => edge.map(idx => idx + startIdx))
        );
      }
      const vertexColors = [...input.vertexColors];
      while (vertexColors.length < input.vertices.length * 4) {
        vertexColors.push(...this.renderer.states.curFillColor);
      }
      this.geometry.vertexColors.push(...vertexColors);
    }

    /**
     * Adds geometry from the renderer's immediate mode into the builder's
     * combined geometry.
     */
    addImmediate(geometry, shapeMode, { validateFaces = false } = {}) {
      const faces = [];

      if (this.renderer.states.fillColor) {
        if (
          shapeMode === TRIANGLE_STRIP ||
          shapeMode === QUAD_STRIP
        ) {
          for (let i = 2; i < geometry.vertices.length; i++) {
            if (i % 2 === 0) {
              faces.push([i, i - 1, i - 2]);
            } else {
              faces.push([i, i - 2, i - 1]);
            }
          }
        } else if (shapeMode === TRIANGLE_FAN) {
          for (let i = 2; i < geometry.vertices.length; i++) {
            faces.push([0, i - 1, i]);
          }
        } else if (shapeMode === TRIANGLES) {
          for (let i = 0; i < geometry.vertices.length; i += 3) {
            if (
              !validateFaces ||
              geometry.vertices[i].copy().sub(geometry.vertices[i+1])
                .cross(geometry.vertices[i].copy().sub(geometry.vertices[i+2]))
                .magSq() > 0
            ) {
              faces.push([i, i + 1, i + 2]);
            }
          }
        }
      }
      this.addGeometry(Object.assign({}, geometry, { faces }));
    }

    /**
     * Adds geometry from the renderer's retained mode into the builder's
     * combined geometry.
     */
    addRetained(geometry) {
      this.addGeometry(geometry);
    }

    /**
     * Cleans up the state of the renderer and returns the combined geometry that
     * was built.
     * @returns p5.Geometry The flattened, combined geometry
     */
    finish() {
      this.renderer._pInst.pop();
      return this.geometry;
    }
  }

  /**
   * Keeps track of how many custom geometry objects have been made so that each
   * can be assigned a unique ID.
   */
  GeometryBuilder.nextGeometryId = 0;

  /**
   * @module Math
   * @submodule Quaternion
   */


  class Quat {
    constructor(w, x, y, z) {
      this.w = w;
      this.vec = new Vector(x, y, z);
    }

    /**
     * Returns a Quaternion for the
     * axis angle representation of the rotation
     *
     * @method fromAxisAngle
     * @param {Number} [angle] Angle with which the points needs to be rotated
     * @param {Number} [x] x component of the axis vector
     * @param {Number} [y] y component of the axis vector
     * @param {Number} [z] z component of the axis vector
     * @chainable
     */
    static fromAxisAngle(angle, x, y, z) {
      const w = Math.cos(angle/2);
      const vec = new Vector(x, y, z).normalize().mult(Math.sin(angle/2));
      return new Quat(w, vec.x, vec.y, vec.z);
    }

    conjugate() {
      return new Quat(this.w, -this.vec.x, -this.vec.y, -this.vec.z);
    }

    /**
     * Multiplies a quaternion with other quaternion.
     * @method mult
     * @param  {p5.Quat} [quat] quaternion to multiply with the quaternion calling the method.
     * @chainable
     */
    multiply(quat) {

      return new Quat(
        this.w * quat.w - this.vec.x * quat.vec.x - this.vec.y * quat.vec.y - this.vec.z - quat.vec.z,
        this.w * quat.vec.x + this.vec.x * quat.w + this.vec.y * quat.vec.z - this.vec.z * quat.vec.y,
        this.w * quat.vec.y - this.vec.x * quat.vec.z + this.vec.y * quat.w + this.vec.z * quat.vec.x,
        this.w * quat.vec.z + this.vec.x * quat.vec.y - this.vec.y * quat.vec.x + this.vec.z * quat.w
      );

    }

    /**
     * This is similar to quaternion multiplication
     * but when multipying vector with quaternion
     * the multiplication can be simplified to the below formula.
     * This was taken from the below stackexchange link
     * https://gamedev.stackexchange.com/questions/28395/rotating-vector3-by-a-quaternion/50545#50545
     * @private
     * @param {p5.Vector} [p] vector to rotate on the axis quaternion
     */
    rotateVector(p) {
      return Vector.mult( p, this.w*this.w - this.vec.dot(this.vec) )
        .add( Vector.mult( this.vec, 2 * p.dot(this.vec) ) )
        .add( Vector.mult( this.vec, 2 * this.w ).cross( p ) )
        .clampToZero();
    }

    /**
     * Rotates the Quaternion by the quaternion passed
     * which contains the axis of roation and angle of rotation
     *
     * @method rotateBy
     * @param {p5.Quat} [axesQuat] axis quaternion which contains
     *  the axis of rotation and angle of rotation
     * @chainable
     */
    rotateBy(axesQuat) {
      return axesQuat.multiply(this).multiply(axesQuat.conjugate()).
        vec.clampToZero();
    }
  }

  function quat(p5, fn){
    /**
     * A class to describe a Quaternion
     * for vector rotations in the p5js webgl renderer.
     * Please refer the following link for details on the implementation
     * https://danceswithcode.net/engineeringnotes/quaternions/quaternions.html
     * @class p5.Quat
     * @constructor
     * @param {Number} [w] Scalar part of the quaternion
     * @param {Number} [x] x component of imaginary part of quaternion
     * @param {Number} [y] y component of imaginary part of quaternion
     * @param {Number} [z] z component of imaginary part of quaternion
     * @private
     */
    p5.Quat = Quat;
  }

  if(typeof p5 !== 'undefined'){
    quat(p5);
  }

  /**
   * @module 3D
   * @submodule Camera
   * @requires core
   */


  class Camera {
    constructor(renderer) {
      this._renderer = renderer;

      this.cameraType = 'default';
      this.useLinePerspective = true;
      this.cameraMatrix = new Matrix(4);
      this.projMatrix = new Matrix(4);
      this.yScale = 1;
    }

    ////////////////////////////////////////////////////////////////////////////////
    // Camera Projection Methods
    ////////////////////////////////////////////////////////////////////////////////

    /**
     * Sets a perspective projection for the camera.
     *
     * In a perspective projection, shapes that are further from the camera appear
     * smaller than shapes that are near the camera. This technique, called
     * foreshortening, creates realistic 3D scenes. It’s applied by default in new
     * `p5.Camera` objects.
     *
     * `myCamera.perspective()` changes the camera’s perspective by changing its
     * viewing frustum. The frustum is the volume of space that’s visible to the
     * camera. The frustum’s shape is a pyramid with its top cut off. The camera
     * is placed where the top of the pyramid should be and points towards the
     * base of the pyramid. It views everything within the frustum.
     *
     * The first parameter, `fovy`, is the camera’s vertical field of view. It’s
     * an angle that describes how tall or narrow a view the camera has. For
     * example, calling `myCamera.perspective(0.5)` sets the camera’s vertical
     * field of view to 0.5 radians. By default, `fovy` is calculated based on the
     * sketch’s height and the camera’s default z-coordinate, which is 800. The
     * formula for the default `fovy` is `2 * atan(height / 2 / 800)`.
     *
     * The second parameter, `aspect`, is the camera’s aspect ratio. It’s a number
     * that describes the ratio of the top plane’s width to its height. For
     * example, calling `myCamera.perspective(0.5, 1.5)` sets the camera’s field
     * of view to 0.5 radians and aspect ratio to 1.5, which would make shapes
     * appear thinner on a square canvas. By default, `aspect` is set to
     * `width / height`.
     *
     * The third parameter, `near`, is the distance from the camera to the near
     * plane. For example, calling `myCamera.perspective(0.5, 1.5, 100)` sets the
     * camera’s field of view to 0.5 radians, its aspect ratio to 1.5, and places
     * the near plane 100 pixels from the camera. Any shapes drawn less than 100
     * pixels from the camera won’t be visible. By default, `near` is set to
     * `0.1 * 800`, which is 1/10th the default distance between the camera and
     * the origin.
     *
     * The fourth parameter, `far`, is the distance from the camera to the far
     * plane. For example, calling `myCamera.perspective(0.5, 1.5, 100, 10000)`
     * sets the camera’s field of view to 0.5 radians, its aspect ratio to 1.5,
     * places the near plane 100 pixels from the camera, and places the far plane
     * 10,000 pixels from the camera. Any shapes drawn more than 10,000 pixels
     * from the camera won’t be visible. By default, `far` is set to `10 * 800`,
     * which is 10 times the default distance between the camera and the origin.
     *
     * @for p5.Camera
     * @param  {Number} [fovy]   camera frustum vertical field of view. Defaults to
     *                           `2 * atan(height / 2 / 800)`.
     * @param  {Number} [aspect] camera frustum aspect ratio. Defaults to
     *                           `width / height`.
     * @param  {Number} [near]   distance from the camera to the near clipping plane.
     *                           Defaults to `0.1 * 800`.
     * @param  {Number} [far]    distance from the camera to the far clipping plane.
     *                           Defaults to `10 * 800`.
     *
     * @example
     * // Double-click to toggle between cameras.
     *
     * let cam1;
     * let cam2;
     * let isDefaultCamera = true;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the first camera.
     *   // Keep its default settings.
     *   cam1 = createCamera();
     *
     *   // Create the second camera.
     *   cam2 = createCamera();
     *
     *   // Place it at the top-right.
     *   cam2.camera(400, -400, 800);
     *
     *   // Set its fovy to 0.2.
     *   // Set its aspect to 1.5.
     *   // Set its near to 600.
     *   // Set its far to 1200.
     *   cam2.perspective(0.2, 1.5, 600, 1200);
     *
     *   // Set the current camera to cam1.
     *   setCamera(cam1);
     *
     *   describe('A white cube on a gray background. The camera toggles between a frontal view and a skewed aerial view when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw the box.
     *   box();
     * }
     *
     * // Toggle the current camera when the user double-clicks.
     * function doubleClicked() {
     *   if (isDefaultCamera === true) {
     *     setCamera(cam2);
     *     isDefaultCamera = false;
     *   } else {
     *     setCamera(cam1);
     *     isDefaultCamera = true;
     *   }
     * }
     *
     * @example
     * // Double-click to toggle between cameras.
     *
     * let cam1;
     * let cam2;
     * let isDefaultCamera = true;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the first camera.
     *   // Keep its default settings.
     *   cam1 = createCamera();
     *
     *   // Create the second camera.
     *   cam2 = createCamera();
     *
     *   // Place it at the top-right.
     *   cam2.camera(400, -400, 800);
     *
     *   // Set its fovy to 0.2.
     *   // Set its aspect to 1.5.
     *   // Set its near to 600.
     *   // Set its far to 1200.
     *   cam2.perspective(0.2, 1.5, 600, 1200);
     *
     *   // Set the current camera to cam1.
     *   setCamera(cam1);
     *
     *   describe('A white cube moves left and right on a gray background. The camera toggles between a frontal and a skewed aerial view when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Translate the origin left and right.
     *   let x = 100 * sin(frameCount * 0.01);
     *   translate(x, 0, 0);
     *
     *   // Draw the box.
     *   box();
     * }
     *
     * // Toggle the current camera when the user double-clicks.
     * function doubleClicked() {
     *   if (isDefaultCamera === true) {
     *     setCamera(cam2);
     *     isDefaultCamera = false;
     *   } else {
     *     setCamera(cam1);
     *     isDefaultCamera = true;
     *   }
     * }
     */
    perspective(fovy, aspect, near, far) {
      const range = this._renderer.zClipRange();
      this.cameraType = arguments.length > 0 ? 'custom' : 'default';
      if (typeof fovy === 'undefined') {
        fovy = this.defaultCameraFOV;
        // this avoids issue where setting angleMode(DEGREES) before calling
        // perspective leads to a smaller than expected FOV (because
        // _computeCameraDefaultSettings computes in radians)
        this.cameraFOV = fovy;
      } else {
        this.cameraFOV = this._renderer._pInst._toRadians(fovy);
      }
      if (typeof aspect === 'undefined') {
        aspect = this.defaultAspectRatio;
      }
      if (typeof near === 'undefined') {
        near = this.defaultCameraNear;
      }
      if (typeof far === 'undefined') {
        far = this.defaultCameraFar;
      }

      if (near <= 0.0001) {
        near = 0.01;
        console.log(
          'Avoid perspective near plane values close to or below 0. ' +
          'Setting value to 0.01.'
        );
      }

      if (far < near) {
        console.log(
          'Perspective far plane value is less than near plane value. ' +
          'Nothing will be shown.'
        );
      }

      this.aspectRatio = aspect;
      this.cameraNear = near;
      this.cameraFar = far;

      this.projMatrix = new Matrix(4);

      const f = 1.0 / Math.tan(this.cameraFOV / 2);
      const nf = 1.0 / (this.cameraNear - this.cameraFar);

      let A, B;
      if (range[0] === 0) {
        // WebGPU clip space, z in [0, 1]
        A = far / (near - far);
        B = (far * near) / (near - far);
      } else {
        // WebGL clip space, z in [-1, 1]
        A = (far + near) * nf;
        B = (2 * far * near) * nf;
      }

      this.projMatrix.set(f / aspect, 0, 0, 0,
        0, -f * this.yScale, 0, 0,
        0, 0, A, -1,
        0, 0, B, 0);

      if (this._isActive()) {
        this._renderer.states.setValue('uPMatrix', this._renderer.states.uPMatrix.clone());
        this._renderer.states.uPMatrix.set(this.projMatrix);
      }
    }

    /**
     * Sets an orthographic projection for the camera.
     *
     * In an orthographic projection, shapes with the same size always appear the
     * same size, regardless of whether they are near or far from the camera.
     *
     * `myCamera.ortho()` changes the camera’s perspective by changing its viewing
     * frustum from a truncated pyramid to a rectangular prism. The frustum is the
     * volume of space that’s visible to the camera. The camera is placed in front
     * of the frustum and views everything within the frustum. `myCamera.ortho()`
     * has six optional parameters to define the viewing frustum.
     *
     * The first four parameters, `left`, `right`, `bottom`, and `top`, set the
     * coordinates of the frustum’s sides, bottom, and top. For example, calling
     * `myCamera.ortho(-100, 100, 200, -200)` creates a frustum that’s 200 pixels
     * wide and 400 pixels tall. By default, these dimensions are set based on
     * the sketch’s width and height, as in
     * `myCamera.ortho(-width / 2, width / 2, -height / 2, height / 2)`.
     *
     * The last two parameters, `near` and `far`, set the distance of the
     * frustum’s near and far plane from the camera. For example, calling
     * `myCamera.ortho(-100, 100, 200, -200, 50, 1000)` creates a frustum that’s
     * 200 pixels wide, 400 pixels tall, starts 50 pixels from the camera, and
     * ends 1,000 pixels from the camera. By default, `near` and `far` are set to
     * 0 and `max(width, height) + 800`, respectively.
     *
     * @for p5.Camera
     * @param  {Number} [left]   x-coordinate of the frustum’s left plane. Defaults to `-width / 2`.
     * @param  {Number} [right]  x-coordinate of the frustum’s right plane. Defaults to `width / 2`.
     * @param  {Number} [bottom] y-coordinate of the frustum’s bottom plane. Defaults to `height / 2`.
     * @param  {Number} [top]    y-coordinate of the frustum’s top plane. Defaults to `-height / 2`.
     * @param  {Number} [near]   z-coordinate of the frustum’s near plane. Defaults to 0.
     * @param  {Number} [far]    z-coordinate of the frustum’s far plane. Defaults to `max(width, height) + 800`.
     *
     * @example
     * // Double-click to toggle between cameras.
     *
     * let cam1;
     * let cam2;
     * let isDefaultCamera = true;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the first camera.
     *   // Keep its default settings.
     *   cam1 = createCamera();
     *
     *   // Create the second camera.
     *   cam2 = createCamera();
     *
     *   // Apply an orthographic projection.
     *   cam2.ortho();
     *
     *   // Set the current camera to cam1.
     *   setCamera(cam1);
     *
     *   describe('A row of white cubes against a gray background. The camera toggles between a perspective and an orthographic projection when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Translate the origin toward the camera.
     *   translate(-10, 10, 500);
     *
     *   // Rotate the coordinate system.
     *   rotateY(-0.1);
     *   rotateX(-0.1);
     *
     *   // Draw the row of boxes.
     *   for (let i = 0; i < 6; i += 1) {
     *     translate(0, 0, -40);
     *     box(10);
     *   }
     * }
     *
     * // Toggle the current camera when the user double-clicks.
     * function doubleClicked() {
     *   if (isDefaultCamera === true) {
     *     setCamera(cam2);
     *     isDefaultCamera = false;
     *   } else {
     *     setCamera(cam1);
     *     isDefaultCamera = true;
     *   }
     * }
     *
     * @example
     * // Double-click to toggle between cameras.
     *
     * let cam1;
     * let cam2;
     * let isDefaultCamera = true;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the first camera.
     *   // Keep its default settings.
     *   cam1 = createCamera();
     *
     *   // Create the second camera.
     *   cam2 = createCamera();
     *
     *   // Apply an orthographic projection.
     *   cam2.ortho();
     *
     *   // Set the current camera to cam1.
     *   setCamera(cam1);
     *
     *   describe('A row of white cubes slither like a snake against a gray background. The camera toggles between a perspective and an orthographic projection when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Translate the origin toward the camera.
     *   translate(-10, 10, 500);
     *
     *   // Rotate the coordinate system.
     *   rotateY(-0.1);
     *   rotateX(-0.1);
     *
     *   // Draw the row of boxes.
     *   for (let i = 0; i < 6; i += 1) {
     *     push();
     *     // Calculate the box's coordinates.
     *     let x = 10 * sin(frameCount * 0.02 + i * 0.6);
     *     let z = -40 * i;
     *     // Translate the origin.
     *     translate(x, 0, z);
     *     // Draw the box.
     *     box(10);
     *     pop();
     *   }
     * }
     *
     * // Toggle the current camera when the user double-clicks.
     * function doubleClicked() {
     *   if (isDefaultCamera === true) {
     *     setCamera(cam2);
     *     isDefaultCamera = false;
     *   } else {
     *     setCamera(cam1);
     *     isDefaultCamera = true;
     *   }
     * }
     */
    ortho(left, right, bottom, top, near, far) {
      const source = this.fbo || this._renderer;
      if (left === undefined) left = -source.width / 2;
      if (right === undefined) right = +source.width / 2;
      if (bottom === undefined) bottom = -source.height / 2;
      if (top === undefined) top = +source.height / 2;
      if (near === undefined) near = 0;
      if (far === undefined) far = Math.max(source.width, source.height) + 800;
      this.cameraNear = near;
      this.cameraFar = far;
      const w = right - left;
      const h = top - bottom;
      const d = far - near;
      const x = 2 / w;
      const y = 2 / h * this.yScale;
      const z = -2 / d;
      const tx = -(right + left) / w;
      const ty = -(top + bottom) / h;
      const tz = -(far + near) / d;
      this.projMatrix = new Matrix(4);

      this.projMatrix.set(x, 0, 0, 0,
        0, -y, 0, 0,
        0, 0, z, 0,
        tx, ty, tz, 1);

      if (this._isActive()) {
        this._renderer.states.setValue('uPMatrix', this._renderer.states.uPMatrix.clone());
        this._renderer.states.uPMatrix.set(this.projMatrix);
      }
      this.cameraType = 'custom';
    }
    /**
     * Sets the camera's frustum.
     *
     * In a frustum projection, shapes that are further from the camera appear
     * smaller than shapes that are near the camera. This technique, called
     * foreshortening, creates realistic 3D scenes.
     *
     * `myCamera.frustum()` changes the camera’s perspective by changing its
     * viewing frustum. The frustum is the volume of space that’s visible to the
     * camera. The frustum’s shape is a pyramid with its top cut off. The camera
     * is placed where the top of the pyramid should be and points towards the
     * base of the pyramid. It views everything within the frustum.
     *
     * The first four parameters, `left`, `right`, `bottom`, and `top`, set the
     * coordinates of the frustum’s sides, bottom, and top. For example, calling
     * `myCamera.frustum(-100, 100, 200, -200)` creates a frustum that’s 200
     * pixels wide and 400 pixels tall. By default, these coordinates are set
     * based on the sketch’s width and height, as in
     * `myCamera.frustum(-width / 20, width / 20, height / 20, -height / 20)`.
     *
     * The last two parameters, `near` and `far`, set the distance of the
     * frustum’s near and far plane from the camera. For example, calling
     * `myCamera.frustum(-100, 100, 200, -200, 50, 1000)` creates a frustum that’s
     * 200 pixels wide, 400 pixels tall, starts 50 pixels from the camera, and ends
     * 1,000 pixels from the camera. By default, near is set to `0.1 * 800`, which
     * is 1/10th the default distance between the camera and the origin. `far` is
     * set to `10 * 800`, which is 10 times the default distance between the
     * camera and the origin.
     *
     * @for p5.Camera
     * @param  {Number} [left]   x-coordinate of the frustum’s left plane. Defaults to `-width / 20`.
     * @param  {Number} [right]  x-coordinate of the frustum’s right plane. Defaults to `width / 20`.
     * @param  {Number} [bottom] y-coordinate of the frustum’s bottom plane. Defaults to `height / 20`.
     * @param  {Number} [top]    y-coordinate of the frustum’s top plane. Defaults to `-height / 20`.
     * @param  {Number} [near]   z-coordinate of the frustum’s near plane. Defaults to `0.1 * 800`.
     * @param  {Number} [far]    z-coordinate of the frustum’s far plane. Defaults to `10 * 800`.
     *
     * @example
     * // Double-click to toggle between cameras.
     *
     * let cam1;
     * let cam2;
     * let isDefaultCamera = true;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the first camera.
     *   // Keep its default settings.
     *   cam1 = createCamera();
     *
     *   // Create the second camera.
     *   cam2 = createCamera();
     *
     *   // Adjust the frustum.
     *   // Center it.
     *   // Set its width and height to 20 pixels.
     *   // Place its near plane 300 pixels from the camera.
     *   // Place its far plane 350 pixels from the camera.
     *   cam2.frustum(-10, 10, -10, 10, 300, 350);
     *
     *   // Set the current camera to cam1.
     *   setCamera(cam1);
     *
     *   describe(
     *     'A row of white cubes against a gray background. The camera zooms in on one cube when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Translate the origin toward the camera.
     *   translate(-10, 10, 600);
     *
     *   // Rotate the coordinate system.
     *   rotateY(-0.1);
     *   rotateX(-0.1);
     *
     *   // Draw the row of boxes.
     *   for (let i = 0; i < 6; i += 1) {
     *     translate(0, 0, -40);
     *     box(10);
     *   }
     * }
     *
     * // Toggle the current camera when the user double-clicks.
     * function doubleClicked() {
     *   if (isDefaultCamera === true) {
     *     setCamera(cam2);
     *     isDefaultCamera = false;
     *   } else {
     *     setCamera(cam1);
     *     isDefaultCamera = true;
     *   }
     * }
     */
    frustum(left, right, bottom, top, near, far) {
      if (left === undefined) left = -this._renderer.width * 0.05;
      if (right === undefined) right = +this._renderer.width * 0.05;
      if (bottom === undefined) bottom = +this._renderer.height * 0.05;
      if (top === undefined) top = -this._renderer.height * 0.05;
      if (near === undefined) near = this.defaultCameraNear;
      if (far === undefined) far = this.defaultCameraFar;

      this.cameraNear = near;
      this.cameraFar = far;

      const w = right - left;
      const h = top - bottom;
      const d = far - near;

      const x = +(2.0 * near) / w;
      const y = +(2.0 * near) / h * this.yScale;
      const z = -(2.0 * far * near) / d;

      const tx = (right + left) / w;
      const ty = (top + bottom) / h;
      const tz = -(far + near) / d;

      this.projMatrix = new Matrix(4);


      this.projMatrix.set(x, 0, 0, 0,
        0, -y, 0, 0,
        tx, ty, tz, -1,
        0, 0, z, 0);


      if (this._isActive()) {
        this._renderer.states.setValue('uPMatrix', this._renderer.states.uPMatrix.clone());
        this._renderer.states.uPMatrix.set(this.projMatrix);
      }

      this.cameraType = 'custom';
    }

    ////////////////////////////////////////////////////////////////////////////////
    // Camera Orientation Methods
    ////////////////////////////////////////////////////////////////////////////////

    /**
     * Rotate camera view about arbitrary axis defined by x,y,z
     * based on http://learnwebgl.brown37.net/07_cameras/camera_rotating_motion.html
     * @private
     */
    _rotateView(a, x, y, z) {
      let centerX = this.centerX;
      let centerY = this.centerY;
      let centerZ = this.centerZ;

      // move center by eye position such that rotation happens around eye position
      centerX -= this.eyeX;
      centerY -= this.eyeY;
      centerZ -= this.eyeZ;

      const rotation = new Matrix(4); // TODO Maybe pass p5
      rotation.rotate4x4(this._renderer._pInst._toRadians(a), x, y, z);

      const rotatedCenter = [
        centerX * rotation.mat4[0] + centerY * rotation.mat4[4] + centerZ * rotation.mat4[8],
        centerX * rotation.mat4[1] + centerY * rotation.mat4[5] + centerZ * rotation.mat4[9],
        centerX * rotation.mat4[2] + centerY * rotation.mat4[6] + centerZ * rotation.mat4[10]
      ];

      // add eye position back into center
      rotatedCenter[0] += this.eyeX;
      rotatedCenter[1] += this.eyeY;
      rotatedCenter[2] += this.eyeZ;

      this.camera(
        this.eyeX,
        this.eyeY,
        this.eyeZ,
        rotatedCenter[0],
        rotatedCenter[1],
        rotatedCenter[2],
        this.upX,
        this.upY,
        this.upZ
      );
    }

    /**
     * Rotates the camera in a clockwise/counter-clockwise direction.
     *
     * Rolling rotates the camera without changing its orientation. The rotation
     * happens in the camera’s "local" space.
     *
     * The parameter, `angle`, is the angle the camera should rotate. Passing a
     * positive angle, as in `myCamera.roll(0.001)`, rotates the camera in counter-clockwise direction.
     * Passing a negative angle, as in `myCamera.roll(-0.001)`, rotates the
     * camera in clockwise direction.
     *
     * Note: Angles are interpreted based on the current
     * <a href="#/p5/angleMode">angleMode()</a>.
     *
     * @method roll
     * @param {Number} angle amount to rotate camera in current
     * <a href="#/p5/angleMode">angleMode</a> units.
     * @example
     * let cam;
     * let delta = 0.01;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *   normalMaterial();
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Roll camera according to angle 'delta'
     *   cam.roll(delta);
     *
     *   translate(0, 0, 0);
     *   box(20);
     *   translate(0, 25, 0);
     *   box(20);
     *   translate(0, 26, 0);
     *   box(20);
     *   translate(0, 27, 0);
     *   box(20);
     *   translate(0, 28, 0);
     *   box(20);
     *   translate(0,29, 0);
     *   box(20);
     *   translate(0, 30, 0);
     *   box(20);
     * }
     *
     * @alt
     * camera view rotates in counter clockwise direction with vertically stacked boxes in front of it.
     */
    roll(amount) {
      const local = this._getLocalAxes();
      const axisQuaternion = Quat.fromAxisAngle(
        this._renderer._pInst._toRadians(amount),
        local.z[0], local.z[1], local.z[2]);
      // const upQuat = new p5.Quat(0, this.upX, this.upY, this.upZ);
      const newUpVector = axisQuaternion.rotateVector(
        new Vector(this.upX, this.upY, this.upZ));
      this.camera(
        this.eyeX,
        this.eyeY,
        this.eyeZ,
        this.centerX,
        this.centerY,
        this.centerZ,
        newUpVector.x,
        newUpVector.y,
        newUpVector.z
      );
    }

    /**
     * Rotates the camera left and right.
     *
     * Panning rotates the camera without changing its position. The rotation
     * happens in the camera’s "local" space.
     *
     * The parameter, `angle`, is the angle the camera should rotate. Passing a
     * positive angle, as in `myCamera.pan(0.001)`, rotates the camera to the
     * right. Passing a negative angle, as in `myCamera.pan(-0.001)`, rotates the
     * camera to the left.
     *
     * Note: Angles are interpreted based on the current
     * <a href="#/p5/angleMode">angleMode()</a>.
     *
     * @param {Number} angle amount to rotate in the current
     *                       <a href="#/p5/angleMode">angleMode()</a>.
     *
     * @example
     * let cam;
     * let delta = 0.001;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-center.
     *   cam.setPosition(0, -400, 800);
     *
     *   // Point the camera at the origin.
     *   cam.lookAt(0, 0, 0);
     *
     *   describe(
     *     'A white cube on a gray background. The cube goes in and out of view as the camera pans left and right.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Pan with the camera.
     *   cam.pan(delta);
     *
     *   // Switch directions every 120 frames.
     *   if (frameCount % 120 === 0) {
     *     delta *= -1;
     *   }
     *
     *   // Draw the box.
     *   box();
     * }
     */
    pan(amount) {
      const local = this._getLocalAxes();
      this._rotateView(amount, local.y[0], local.y[1], local.y[2]);
    }

    /**
     * Rotates the camera up and down.
     *
     * Tilting rotates the camera without changing its position. The rotation
     * happens in the camera’s "local" space.
     *
     * The parameter, `angle`, is the angle the camera should rotate. Passing a
     * positive angle, as in `myCamera.tilt(0.001)`, rotates the camera down.
     * Passing a negative angle, as in `myCamera.tilt(-0.001)`, rotates the camera
     * up.
     *
     * Note: Angles are interpreted based on the current
     * <a href="#/p5/angleMode">angleMode()</a>.
     *
     * @param {Number} angle amount to rotate in the current
     *                       <a href="#/p5/angleMode">angleMode()</a>.
     *
     * @example
     * let cam;
     * let delta = 0.001;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-center.
     *   cam.setPosition(0, -400, 800);
     *
     *   // Point the camera at the origin.
     *   cam.lookAt(0, 0, 0);
     *
     *   describe(
     *     'A white cube on a gray background. The cube goes in and out of view as the camera tilts up and down.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Pan with the camera.
     *   cam.tilt(delta);
     *
     *   // Switch directions every 120 frames.
     *   if (frameCount % 120 === 0) {
     *     delta *= -1;
     *   }
     *
     *   // Draw the box.
     *   box();
     * }
     */
    tilt(amount) {
      const local = this._getLocalAxes();
      this._rotateView(amount, local.x[0], local.x[1], local.x[2]);
    }

    /**
     * Points the camera at a location.
     *
     * `myCamera.lookAt()` changes the camera’s orientation without changing its
     * position.
     *
     * The parameters, `x`, `y`, and `z`, are the coordinates in "world" space
     * where the camera should point. For example, calling
     * `myCamera.lookAt(10, 20, 30)` points the camera at the coordinates
     * `(10, 20, 30)`.
     *
     * @for p5.Camera
     * @param {Number} x x-coordinate of the position where the camera should look in "world" space.
     * @param {Number} y y-coordinate of the position where the camera should look in "world" space.
     * @param {Number} z z-coordinate of the position where the camera should look in "world" space.
     *
     * @example
     * // Double-click to look at a different cube.
     *
     * let cam;
     * let isLookingLeft = true;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-center.
     *   cam.setPosition(0, -400, 800);
     *
     *   // Point the camera at the origin.
     *   cam.lookAt(-30, 0, 0);
     *
     *   describe(
     *     'A red cube and a blue cube on a gray background. The camera switches focus between the cubes when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw the box on the left.
     *   push();
     *   // Translate the origin to the left.
     *   translate(-30, 0, 0);
     *   // Style the box.
     *   fill(255, 0, 0);
     *   // Draw the box.
     *   box(20);
     *   pop();
     *
     *   // Draw the box on the right.
     *   push();
     *   // Translate the origin to the right.
     *   translate(30, 0, 0);
     *   // Style the box.
     *   fill(0, 0, 255);
     *   // Draw the box.
     *   box(20);
     *   pop();
     * }
     *
     * // Change the camera's focus when the user double-clicks.
     * function doubleClicked() {
     *   if (isLookingLeft === true) {
     *     cam.lookAt(30, 0, 0);
     *     isLookingLeft = false;
     *   } else {
     *     cam.lookAt(-30, 0, 0);
     *     isLookingLeft = true;
     *   }
     * }
     */
    lookAt(x, y, z) {
      this.camera(
        this.eyeX,
        this.eyeY,
        this.eyeZ,
        x,
        y,
        z,
        this.upX,
        this.upY,
        this.upZ
      );
    }

    ////////////////////////////////////////////////////////////////////////////////
    // Camera Position Methods
    ////////////////////////////////////////////////////////////////////////////////

    /**
     * Sets the position and orientation of the camera.
     *
     * `myCamera.camera()` allows objects to be viewed from different angles. It
     * has nine parameters that are all optional.
     *
     * The first three parameters, `x`, `y`, and `z`, are the coordinates of the
     * camera’s position in "world" space. For example, calling
     * `myCamera.camera(0, 0, 0)` places the camera at the origin `(0, 0, 0)`. By
     * default, the camera is placed at `(0, 0, 800)`.
     *
     * The next three parameters, `centerX`, `centerY`, and `centerZ` are the
     * coordinates of the point where the camera faces in "world" space. For
     * example, calling `myCamera.camera(0, 0, 0, 10, 20, 30)` places the camera
     * at the origin `(0, 0, 0)` and points it at `(10, 20, 30)`. By default, the
     * camera points at the origin `(0, 0, 0)`.
     *
     * The last three parameters, `upX`, `upY`, and `upZ` are the components of
     * the "up" vector in "local" space. The "up" vector orients the camera’s
     * y-axis. For example, calling
     * `myCamera.camera(0, 0, 0, 10, 20, 30, 0, -1, 0)` places the camera at the
     * origin `(0, 0, 0)`, points it at `(10, 20, 30)`, and sets the "up" vector
     * to `(0, -1, 0)` which is like holding it upside-down. By default, the "up"
     * vector is `(0, 1, 0)`.
     *
     * @for p5.Camera
     * @param  {Number} [x]        x-coordinate of the camera. Defaults to 0.
     * @param  {Number} [y]        y-coordinate of the camera. Defaults to 0.
     * @param  {Number} [z]        z-coordinate of the camera. Defaults to 800.
     * @param  {Number} [centerX]  x-coordinate of the point the camera faces. Defaults to 0.
     * @param  {Number} [centerY]  y-coordinate of the point the camera faces. Defaults to 0.
     * @param  {Number} [centerZ]  z-coordinate of the point the camera faces. Defaults to 0.
     * @param  {Number} [upX]      x-component of the camera’s "up" vector. Defaults to 0.
     * @param  {Number} [upY]      x-component of the camera’s "up" vector. Defaults to 1.
     * @param  {Number} [upZ]      z-component of the camera’s "up" vector. Defaults to 0.
     *
     * @example
     * // Double-click to toggle between cameras.
     *
     * let cam1;
     * let cam2;
     * let isDefaultCamera = true;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the first camera.
     *   // Keep its default settings.
     *   cam1 = createCamera();
     *
     *   // Create the second camera.
     *   cam2 = createCamera();
     *
     *   // Place it at the top-right: (1200, -600, 100)
     *   // Point it at the row of boxes: (-10, -10, 400)
     *   // Set its "up" vector to the default: (0, 1, 0)
     *   cam2.camera(1200, -600, 100, -10, -10, 400, 0, 1, 0);
     *
     *   // Set the current camera to cam1.
     *   setCamera(cam1);
     *
     *   describe(
     *     'A row of white cubes against a gray background. The camera toggles between a frontal and an aerial view when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Translate the origin toward the camera.
     *   translate(-10, 10, 500);
     *
     *   // Rotate the coordinate system.
     *   rotateY(-0.1);
     *   rotateX(-0.1);
     *
     *   // Draw the row of boxes.
     *   for (let i = 0; i < 6; i += 1) {
     *     translate(0, 0, -30);
     *     box(10);
     *   }
     * }
     *
     * // Toggle the current camera when the user double-clicks.
     * function doubleClicked() {
     *   if (isDefaultCamera === true) {
     *     setCamera(cam2);
     *     isDefaultCamera = false;
     *   } else {
     *     setCamera(cam1);
     *     isDefaultCamera = true;
     *   }
     * }
     *
     * @example
     * // Double-click to toggle between cameras.
     *
     * let cam1;
     * let cam2;
     * let isDefaultCamera = true;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the first camera.
     *   // Keep its default settings.
     *   cam1 = createCamera();
     *
     *   // Create the second camera.
     *   cam2 = createCamera();
     *
     *   // Place it at the right: (1200, 0, 100)
     *   // Point it at the row of boxes: (-10, -10, 400)
     *   // Set its "up" vector to the default: (0, 1, 0)
     *   cam2.camera(1200, 0, 100, -10, -10, 400, 0, 1, 0);
     *
     *   // Set the current camera to cam1.
     *   setCamera(cam1);
     *
     *   describe(
     *     'A row of white cubes against a gray background. The camera toggles between a static frontal view and an orbiting view when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Update cam2's position.
     *   let x = 1200 * cos(frameCount * 0.01);
     *   let y = -600 * sin(frameCount * 0.01);
     *   cam2.camera(x, y, 100, -10, -10, 400, 0, 1, 0);
     *
     *   // Translate the origin toward the camera.
     *   translate(-10, 10, 500);
     *
     *   // Rotate the coordinate system.
     *   rotateY(-0.1);
     *   rotateX(-0.1);
     *
     *   // Draw the row of boxes.
     *   for (let i = 0; i < 6; i += 1) {
     *     translate(0, 0, -30);
     *     box(10);
     *   }
     * }
     *
     * // Toggle the current camera when the user double-clicks.
     * function doubleClicked() {
     *   if (isDefaultCamera === true) {
     *     setCamera(cam2);
     *     isDefaultCamera = false;
     *   } else {
     *     setCamera(cam1);
     *     isDefaultCamera = true;
     *   }
     * }
     */
    camera(
      eyeX,
      eyeY,
      eyeZ,
      centerX,
      centerY,
      centerZ,
      upX,
      upY,
      upZ
    ) {
      if (typeof eyeX === 'undefined') {
        eyeX = this.defaultEyeX;
        eyeY = this.defaultEyeY;
        eyeZ = this.defaultEyeZ;
        centerX = eyeX;
        centerY = eyeY;
        centerZ = 0;
        upX = 0;
        upY = 1;
        upZ = 0;
      }

      this.eyeX = eyeX;
      this.eyeY = eyeY;
      this.eyeZ = eyeZ;

      if (typeof centerX !== 'undefined') {
        this.centerX = centerX;
        this.centerY = centerY;
        this.centerZ = centerZ;
      }

      if (typeof upX !== 'undefined') {
        this.upX = upX;
        this.upY = upY;
        this.upZ = upZ;
      }

      const local = this._getLocalAxes();

      // the camera affects the model view matrix, insofar as it
      // inverse translates the world to the eye position of the camera
      // and rotates it.

      this.cameraMatrix.set(local.x[0], local.y[0], local.z[0], 0,
        local.x[1], local.y[1], local.z[1], 0,
        local.x[2], local.y[2], local.z[2], 0,
        0, 0, 0, 1);


      const tx = -eyeX;
      const ty = -eyeY;
      const tz = -eyeZ;

      this.cameraMatrix.translate([tx, ty, tz]);

      if (this._isActive()) {
        this._renderer.states.setValue('uViewMatrix', this._renderer.states.uViewMatrix.clone());
        this._renderer.states.uViewMatrix.set(this.cameraMatrix);
      }
      return this;
    }

    /**
     * Moves the camera along its "local" axes without changing its orientation.
     *
     * The parameters, `x`, `y`, and `z`, are the distances the camera should
     * move. For example, calling `myCamera.move(10, 20, 30)` moves the camera 10
     * pixels to the right, 20 pixels down, and 30 pixels backward in its "local"
     * space.
     *
     * @param {Number} x distance to move along the camera’s "local" x-axis.
     * @param {Number} y distance to move along the camera’s "local" y-axis.
     * @param {Number} z distance to move along the camera’s "local" z-axis.
     * @example
     * // Click the canvas to begin detecting key presses.
     *
     * let cam;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the first camera.
     *   // Keep its default settings.
     *   cam = createCamera();
     *
     *   // Place the camera at the top-right.
     *   cam.setPosition(400, -400, 800);
     *
     *   // Point it at the origin.
     *   cam.lookAt(0, 0, 0);
     *
     *   // Set the camera.
     *   setCamera(cam);
     *
     *   describe(
     *     'A white cube drawn against a gray background. The cube appears to move when the user presses certain keys.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Move the camera along its "local" axes
     *   // when the user presses certain keys.
     *
     *   // Move horizontally.
     *   if (keyIsDown(LEFT_ARROW)) {
     *     cam.move(-1, 0, 0);
     *   }
     *   if (keyIsDown(RIGHT_ARROW)) {
     *     cam.move(1, 0, 0);
     *   }
     *
     *   // Move vertically.
     *   if (keyIsDown(UP_ARROW)) {
     *     cam.move(0, -1, 0);
     *   }
     *   if (keyIsDown(DOWN_ARROW)) {
     *     cam.move(0, 1, 0);
     *   }
     *
     *   // Move in/out of the screen.
     *   if (keyIsDown('i')) {
     *     cam.move(0, 0, -1);
     *   }
     *   if (keyIsDown('o')) {
     *     cam.move(0, 0, 1);
     *   }
     *
     *   // Draw the box.
     *   box();
     * }
     */
    move(x, y, z) {
      const local = this._getLocalAxes();

      // scale local axes by movement amounts
      // based on http://learnwebgl.brown37.net/07_cameras/camera_linear_motion.html
      const dx = [local.x[0] * x, local.x[1] * x, local.x[2] * x];
      const dy = [local.y[0] * y, local.y[1] * y, local.y[2] * y];
      const dz = [local.z[0] * z, local.z[1] * z, local.z[2] * z];

      this.camera(
        this.eyeX + dx[0] + dy[0] + dz[0],
        this.eyeY + dx[1] + dy[1] + dz[1],
        this.eyeZ + dx[2] + dy[2] + dz[2],
        this.centerX + dx[0] + dy[0] + dz[0],
        this.centerY + dx[1] + dy[1] + dz[1],
        this.centerZ + dx[2] + dy[2] + dz[2],
        this.upX,
        this.upY,
        this.upZ
      );
    }

    /**
     * Sets the camera’s position in "world" space without changing its
     * orientation.
     *
     * The parameters, `x`, `y`, and `z`, are the coordinates where the camera
     * should be placed. For example, calling `myCamera.setPosition(10, 20, 30)`
     * places the camera at coordinates `(10, 20, 30)` in "world" space.
     *
     * @param {Number} x x-coordinate in "world" space.
     * @param {Number} y y-coordinate in "world" space.
     * @param {Number} z z-coordinate in "world" space.
     *
     * @example
     * // Double-click to toggle between cameras.
     *
     * let cam1;
     * let cam2;
     * let isDefaultCamera = true;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the first camera.
     *   // Keep its default settings.
     *   cam1 = createCamera();
     *
     *   // Create the second camera.
     *   cam2 = createCamera();
     *
     *   // Place it closer to the origin.
     *   cam2.setPosition(0, 0, 600);
     *
     *   // Set the current camera to cam1.
     *   setCamera(cam1);
     *
     *   describe(
     *     'A row of white cubes against a gray background. The camera toggles the amount of zoom when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Translate the origin toward the camera.
     *   translate(-10, 10, 500);
     *
     *   // Rotate the coordinate system.
     *   rotateY(-0.1);
     *   rotateX(-0.1);
     *
     *   // Draw the row of boxes.
     *   for (let i = 0; i < 6; i += 1) {
     *     translate(0, 0, -30);
     *     box(10);
     *   }
     * }
     *
     * // Toggle the current camera when the user double-clicks.
     * function doubleClicked() {
     *   if (isDefaultCamera === true) {
     *     setCamera(cam2);
     *     isDefaultCamera = false;
     *   } else {
     *     setCamera(cam1);
     *     isDefaultCamera = true;
     *   }
     * }
     *
     * @example
     * // Double-click to toggle between cameras.
     *
     * let cam1;
     * let cam2;
     * let isDefaultCamera = true;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the first camera.
     *   // Keep its default settings.
     *   cam1 = createCamera();
     *
     *   // Create the second camera.
     *   cam2 = createCamera();
     *
     *   // Place it closer to the origin.
     *   cam2.setPosition(0, 0, 600);
     *
     *   // Set the current camera to cam1.
     *   setCamera(cam1);
     *
     *   describe(
     *     'A row of white cubes against a gray background. The camera toggles between a static view and a view that zooms in and out when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Update cam2's z-coordinate.
     *   let z = 100 * sin(frameCount * 0.01) + 700;
     *   cam2.setPosition(0, 0, z);
     *
     *   // Translate the origin toward the camera.
     *   translate(-10, 10, 500);
     *
     *   // Rotate the coordinate system.
     *   rotateY(-0.1);
     *   rotateX(-0.1);
     *
     *   // Draw the row of boxes.
     *   for (let i = 0; i < 6; i += 1) {
     *     translate(0, 0, -30);
     *     box(10);
     *   }
     * }
     *
     * // Toggle the current camera when the user double-clicks.
     * function doubleClicked() {
     *   if (isDefaultCamera === true) {
     *     setCamera(cam2);
     *     isDefaultCamera = false;
     *   } else {
     *     setCamera(cam1);
     *     isDefaultCamera = true;
     *   }
     * }
     */
    setPosition(x, y, z) {
      const diffX = x - this.eyeX;
      const diffY = y - this.eyeY;
      const diffZ = z - this.eyeZ;

      this.camera(
        x,
        y,
        z,
        this.centerX + diffX,
        this.centerY + diffY,
        this.centerZ + diffZ,
        this.upX,
        this.upY,
        this.upZ
      );
    }

    /**
     * Sets the camera’s position, orientation, and projection by copying another
     * camera.
     *
     * The parameter, `cam`, is the `p5.Camera` object to copy. For example, calling
     * `cam2.set(cam1)` will set `cam2` using `cam1`’s configuration.
     *
     * @param {p5.Camera} cam camera to copy.
     *
     * @example
     * // Double-click to "reset" the camera zoom.
     *
     * let cam1;
     * let cam2;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the first camera.
     *   cam1 = createCamera();
     *
     *   // Place the camera at the top-right.
     *   cam1.setPosition(400, -400, 800);
     *
     *   // Point it at the origin.
     *   cam1.lookAt(0, 0, 0);
     *
     *   // Create the second camera.
     *   cam2 = createCamera();
     *
     *   // Copy cam1's configuration.
     *   cam2.set(cam1);
     *
     *   // Set the camera.
     *   setCamera(cam2);
     *
     *   describe(
     *     'A white cube drawn against a gray background. The camera slowly moves forward. The camera resets when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Update cam2's position.
     *   cam2.move(0, 0, -1);
     *
     *   // Draw the box.
     *   box();
     * }
     *
     * // "Reset" the camera when the user double-clicks.
     * function doubleClicked() {
     *   cam2.set(cam1);
     * }
     */
    set(cam) {
      const keyNamesOfThePropToCopy = [
        'eyeX', 'eyeY', 'eyeZ',
        'centerX', 'centerY', 'centerZ',
        'upX', 'upY', 'upZ',
        'cameraFOV', 'aspectRatio', 'cameraNear', 'cameraFar', 'cameraType',
        'yScale', 'useLinePerspective'
      ];
      for (const keyName of keyNamesOfThePropToCopy) {
        this[keyName] = cam[keyName];
      }

      this.cameraMatrix = cam.cameraMatrix.copy();
      this.projMatrix = cam.projMatrix.copy();

      if (this._isActive()) {
        this._renderer.states.setValue('uModelMatrix', this._renderer.states.uModelMatrix.clone());
        this._renderer.states.setValue('uViewMatrix', this._renderer.states.uViewMatrix.clone());
        this._renderer.states.setValue('uPMatrix', this._renderer.states.uPMatrix.clone());
        this._renderer.states.uModelMatrix.reset();
        this._renderer.states.uViewMatrix.set(this.cameraMatrix);
        this._renderer.states.uPMatrix.set(this.projMatrix);
      }
    }
    /**
     * Sets the camera’s position and orientation to values that are in-between
     * those of two other cameras.
     *
     * `myCamera.slerp()` uses spherical linear interpolation to calculate a
     * position and orientation that’s in-between two other cameras. Doing so is
     * helpful for transitioning smoothly between two perspectives.
     *
     * The first two parameters, `cam0` and `cam1`, are the `p5.Camera` objects
     * that should be used to set the current camera.
     *
     * The third parameter, `amt`, is the amount to interpolate between `cam0` and
     * `cam1`. 0.0 keeps the camera’s position and orientation equal to `cam0`’s,
     * 0.5 sets them halfway between `cam0`’s and `cam1`’s , and 1.0 sets the
     * position and orientation equal to `cam1`’s.
     *
     * For example, calling `myCamera.slerp(cam0, cam1, 0.1)` sets cam’s position
     * and orientation very close to `cam0`’s. Calling
     * `myCamera.slerp(cam0, cam1, 0.9)` sets cam’s position and orientation very
     * close to `cam1`’s.
     *
     * Note: All of the cameras must use the same projection.
     *
     * @param {p5.Camera} cam0 first camera.
     * @param {p5.Camera} cam1 second camera.
     * @param {Number} amt amount of interpolation between 0.0 (`cam0`) and 1.0 (`cam1`).
     *
     * @example
     * let cam;
     * let cam0;
     * let cam1;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the main camera.
     *   // Keep its default settings.
     *   cam = createCamera();
     *
     *   // Create the first camera.
     *   // Keep its default settings.
     *   cam0 = createCamera();
     *
     *   // Create the second camera.
     *   cam1 = createCamera();
     *
     *   // Place it at the top-right.
     *   cam1.setPosition(400, -400, 800);
     *
     *   // Point it at the origin.
     *   cam1.lookAt(0, 0, 0);
     *
     *   // Set the current camera to cam.
     *   setCamera(cam);
     *
     *   describe('A white cube drawn against a gray background. The camera slowly oscillates between a frontal view and an aerial view.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the amount to interpolate between cam0 and cam1.
     *   let amt = 0.5 * sin(frameCount * 0.01) + 0.5;
     *
     *   // Update the main camera's position and orientation.
     *   cam.slerp(cam0, cam1, amt);
     *
     *   box();
     * }
     */
    slerp(cam0, cam1, amt) {
      // If t is 0 or 1, do not interpolate and set the argument camera.
      if (amt === 0) {
        this.set(cam0);
        return;
      } else if (amt === 1) {
        this.set(cam1);
        return;
      }

      // For this cameras is ortho, assume that cam0 and cam1 are also ortho
      // and interpolate the elements of the projection matrix.
      // Use logarithmic interpolation for interpolation.
      if (this.projMatrix.mat4[15] !== 0) {
        this.projMatrix.setElement(
          0,
          cam0.projMatrix.mat4[0] *
              Math.pow(cam1.projMatrix.mat4[0] / cam0.projMatrix.mat4[0], amt)
        );
        this.projMatrix.setElement(
          5,
          cam0.projMatrix.mat4[5] *
              Math.pow(cam1.projMatrix.mat4[5] / cam0.projMatrix.mat4[5], amt)
        );
        // If the camera is active, make uPMatrix reflect changes in projMatrix.
        if (this._isActive()) {
          this._renderer.states.setValue('uPMatrix', this.projMatrix.clone());
        }
      }

      // prepare eye vector and center vector of argument cameras.
      const eye0 = new Vector(cam0.eyeX, cam0.eyeY, cam0.eyeZ);
      const eye1 = new Vector(cam1.eyeX, cam1.eyeY, cam1.eyeZ);
      const center0 = new Vector(cam0.centerX, cam0.centerY, cam0.centerZ);
      const center1 = new Vector(cam1.centerX, cam1.centerY, cam1.centerZ);

      // Calculate the distance between eye and center for each camera.
      // Logarithmically interpolate these with amt.
      const dist0 = Vector.dist(eye0, center0);
      const dist1 = Vector.dist(eye1, center1);
      const lerpedDist = dist0 * Math.pow(dist1 / dist0, amt);

      // Next, calculate the ratio to interpolate the eye and center by a constant
      // ratio for each camera. This ratio is the same for both. Also, with this ratio
      // of points, the distance is the minimum distance of the two points of
      // the same ratio.
      // With this method, if the viewpoint is fixed, linear interpolation is performed
      // at the viewpoint, and if the center is fixed, linear interpolation is performed
      // at the center, resulting in reasonable interpolation. If both move, the point
      // halfway between them is taken.
      const eyeDiff = Vector.sub(eye0, eye1);
      const diffDiff = eye0.copy().sub(eye1).sub(center0).add(center1);
      // Suppose there are two line segments. Consider the distance between the points
      // above them as if they were taken in the same ratio. This calculation figures out
      // a ratio that minimizes this.
      // Each line segment is, a line segment connecting the viewpoint and the center
      // for each camera.
      const divider = diffDiff.magSq();
      let ratio = 1; // default.
      if (divider > 0.000001) {
        ratio = Vector.dot(eyeDiff, diffDiff) / divider;
        ratio = Math.max(0, Math.min(ratio, 1));
      }

      // Take the appropriate proportions and work out the points
      // that are between the new viewpoint and the new center position.
      const lerpedMedium = Vector.lerp(
        Vector.lerp(eye0, center0, ratio),
        Vector.lerp(eye1, center1, ratio),
        amt
      );

      // Prepare each of rotation matrix from their camera matrix
      const rotMat0 = cam0.cameraMatrix.createSubMatrix3x3();
      const rotMat1 = cam1.cameraMatrix.createSubMatrix3x3();

      // get front and up vector from local-coordinate-system.
      const front0 = rotMat0.row(2);
      const front1 = rotMat1.row(2);
      const up0 = rotMat0.row(1);
      const up1 = rotMat1.row(1);

      // prepare new vectors.
      const newFront = new Vector();
      const newUp = new Vector();
      const newEye = new Vector();
      const newCenter = new Vector();

      // Create the inverse matrix of mat0 by transposing mat0,
      // and multiply it to mat1 from the right.
      // This matrix represents the difference between the two.
      // 'deltaRot' means 'difference of rotation matrices'.
      const deltaRot = rotMat1.mult(rotMat0.copy().transpose()); // mat1 is 3x3

      // Calculate the trace and from it the cos value of the angle.
      // An orthogonal matrix is just an orthonormal basis. If this is not the identity
      // matrix, it is a centered orthonormal basis plus some angle of rotation about
      // some axis. That's the angle. Letting this be theta, trace becomes 1+2cos(theta).
      // reference: https://en.wikipedia.org/wiki/Rotation_matrix#Determining_the_angle
      const diag = deltaRot.diagonal();
      let cosTheta = 0.5 * (diag[0] + diag[1] + diag[2] - 1);

      // If the angle is close to 0, the two matrices are very close,
      // so in that case we execute linearly interpolate.
      if (1 - cosTheta < 0.0000001) {
        // Obtain the front vector and up vector by linear interpolation
        // and normalize them.
        // calculate newEye, newCenter with newFront vector.
        newFront.set(Vector.lerp(front0, front1, amt)).normalize();

        newEye.set(newFront).mult(ratio * lerpedDist).add(lerpedMedium);
        newCenter.set(newFront).mult((ratio - 1) * lerpedDist).add(lerpedMedium);

        newUp.set(Vector.lerp(up0, up1, amt)).normalize();

        // set the camera
        this.camera(
          newEye.x, newEye.y, newEye.z,
          newCenter.x, newCenter.y, newCenter.z,
          newUp.x, newUp.y, newUp.z
        );
        return;
      }

      // Calculates the axis vector and the angle of the difference orthogonal matrix.
      // The axis vector is what I explained earlier in the comments.
      // similar calculation is here:
      // https://github.com/mrdoob/three.js/blob/883249620049d1632e8791732808fefd1a98c871/src/math/Quaternion.js#L294
      let a, b, c, sinTheta;
      let invOneMinusCosTheta = 1 / (1 - cosTheta);
      const maxDiag = Math.max(diag[0], diag[1], diag[2]);
      const offDiagSum13 = deltaRot.mat3[1] + deltaRot.mat3[3];
      const offDiagSum26 = deltaRot.mat3[2] + deltaRot.mat3[6];
      const offDiagSum57 = deltaRot.mat3[5] + deltaRot.mat3[7];

      if (maxDiag === diag[0]) {
        a = Math.sqrt((diag[0] - cosTheta) * invOneMinusCosTheta); // not zero.
        invOneMinusCosTheta /= a;
        b = 0.5 * offDiagSum13 * invOneMinusCosTheta;
        c = 0.5 * offDiagSum26 * invOneMinusCosTheta;
        sinTheta = 0.5 * (deltaRot.mat3[7] - deltaRot.mat3[5]) / a;

      } else if (maxDiag === diag[1]) {
        b = Math.sqrt((diag[1] - cosTheta) * invOneMinusCosTheta); // not zero.
        invOneMinusCosTheta /= b;
        c = 0.5 * offDiagSum57 * invOneMinusCosTheta;
        a = 0.5 * offDiagSum13 * invOneMinusCosTheta;
        sinTheta = 0.5 * (deltaRot.mat3[2] - deltaRot.mat3[6]) / b;

      } else {
        c = Math.sqrt((diag[2] - cosTheta) * invOneMinusCosTheta); // not zero.
        invOneMinusCosTheta /= c;
        a = 0.5 * offDiagSum26 * invOneMinusCosTheta;
        b = 0.5 * offDiagSum57 * invOneMinusCosTheta;
        sinTheta = 0.5 * (deltaRot.mat3[3] - deltaRot.mat3[1]) / c;
      }

      // Constructs a new matrix after interpolating the angles.
      // Multiplying mat0 by the first matrix yields mat1, but by creating a state
      // in the middle of that matrix, you can obtain a matrix that is
      // an intermediate state between mat0 and mat1.
      const angle = amt * Math.atan2(sinTheta, cosTheta);
      const cosAngle = Math.cos(angle);
      const sinAngle = Math.sin(angle);
      const oneMinusCosAngle = 1 - cosAngle;
      const ab = a * b;
      const bc = b * c;
      const ca = c * a;
      // 3x3
      const lerpedRotMat = new Matrix( [
        cosAngle + oneMinusCosAngle * a * a,
        oneMinusCosAngle * ab + sinAngle * c,
        oneMinusCosAngle * ca - sinAngle * b,
        oneMinusCosAngle * ab - sinAngle * c,
        cosAngle + oneMinusCosAngle * b * b,
        oneMinusCosAngle * bc + sinAngle * a,
        oneMinusCosAngle * ca + sinAngle * b,
        oneMinusCosAngle * bc - sinAngle * a,
        cosAngle + oneMinusCosAngle * c * c
      ]);

      // Multiply this to mat0 from left to get the interpolated front vector.
      // calculate newEye, newCenter with newFront vector.
      lerpedRotMat.multiplyVec(front0, newFront); // this is vec3

      newEye.set(newFront).mult(ratio * lerpedDist).add(lerpedMedium);
      newCenter.set(newFront).mult((ratio - 1) * lerpedDist).add(lerpedMedium);

      lerpedRotMat.multiplyVec(up0, newUp); // this is vec3

      // We also get the up vector in the same way and set the camera.
      // The eye position and center position are calculated based on the front vector.
      this.camera(
        newEye.x, newEye.y, newEye.z,
        newCenter.x, newCenter.y, newCenter.z,
        newUp.x, newUp.y, newUp.z
      );
    }

    ////////////////////////////////////////////////////////////////////////////////
    // Camera Helper Methods
    ////////////////////////////////////////////////////////////////////////////////

    // @TODO: combine this function with _setDefaultCamera to compute these values
    // as-needed
    _computeCameraDefaultSettings() {
      this.defaultAspectRatio = this._renderer.width / this._renderer.height;
      this.defaultEyeX = 0;
      this.defaultEyeY = 0;
      this.defaultEyeZ = 800;
      this.defaultCameraFOV =
        2 * Math.atan(this._renderer.height / 2 / this.defaultEyeZ);
      this.defaultCenterX = 0;
      this.defaultCenterY = 0;
      this.defaultCenterZ = 0;
      this.defaultCameraNear = this.defaultEyeZ * this._renderer.defaultNearScale();
      this.defaultCameraFar = this.defaultEyeZ * this._renderer.defaultFarScale();
    }

    //detect if user didn't set the camera
    //then call this function below
    _setDefaultCamera() {
      this.cameraFOV = this.defaultCameraFOV;
      this.aspectRatio = this.defaultAspectRatio;
      this.eyeX = this.defaultEyeX;
      this.eyeY = this.defaultEyeY;
      this.eyeZ = this.defaultEyeZ;
      this.centerX = this.defaultCenterX;
      this.centerY = this.defaultCenterY;
      this.centerZ = this.defaultCenterZ;
      this.upX = 0;
      this.upY = 1;
      this.upZ = 0;
      this.cameraNear = this.defaultCameraNear;
      this.cameraFar = this.defaultCameraFar;

      this.perspective();
      this.camera();

      this.cameraType = 'default';
    }

    _resize() {
      // If we're using the default camera, update the aspect ratio
      if (this.cameraType === 'default') {
        this._computeCameraDefaultSettings();
        this.cameraFOV = this.defaultCameraFOV;
        this.aspectRatio = this.defaultAspectRatio;
        this.perspective();
      }
    }

    /**
     * Returns a copy of a camera.
     * @private
     */
    copy() {
      const _cam = new Camera(this._renderer);
      _cam.cameraFOV = this.cameraFOV;
      _cam.aspectRatio = this.aspectRatio;
      _cam.eyeX = this.eyeX;
      _cam.eyeY = this.eyeY;
      _cam.eyeZ = this.eyeZ;
      _cam.centerX = this.centerX;
      _cam.centerY = this.centerY;
      _cam.centerZ = this.centerZ;
      _cam.upX = this.upX;
      _cam.upY = this.upY;
      _cam.upZ = this.upZ;
      _cam.cameraNear = this.cameraNear;
      _cam.cameraFar = this.cameraFar;

      _cam.cameraType = this.cameraType;
      _cam.useLinePerspective = this.useLinePerspective;

      _cam.cameraMatrix = this.cameraMatrix.copy();
      _cam.projMatrix = this.projMatrix.copy();
      _cam.yScale = this.yScale;

      _cam.cameraType = this.cameraType;

      return _cam;
    }

    clone() {
      return this.copy();
    }

    /**
     * Returns a camera's local axes: left-right, up-down, and forward-backward,
     * as defined by vectors in world-space.
     * @private
     */
    _getLocalAxes() {
      // calculate camera local Z vector
      let z0 = this.eyeX - this.centerX;
      let z1 = this.eyeY - this.centerY;
      let z2 = this.eyeZ - this.centerZ;

      // normalize camera local Z vector
      const eyeDist = Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2);
      if (eyeDist !== 0) {
        z0 /= eyeDist;
        z1 /= eyeDist;
        z2 /= eyeDist;
      }

      // calculate camera Y vector
      let y0 = this.upX;
      let y1 = this.upY;
      let y2 = this.upZ;

      // compute camera local X vector as up vector (local Y) cross local Z
      let x0 = y1 * z2 - y2 * z1;
      let x1 = -y0 * z2 + y2 * z0;
      let x2 = y0 * z1 - y1 * z0;

      // recompute y = z cross x
      y0 = z1 * x2 - z2 * x1;
      y1 = -z0 * x2 + z2 * x0;
      y2 = z0 * x1 - z1 * x0;

      // cross product gives area of parallelogram, which is < 1.0 for
      // non-perpendicular unit-length vectors; so normalize x, y here:
      const xmag = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2);
      if (xmag !== 0) {
        x0 /= xmag;
        x1 /= xmag;
        x2 /= xmag;
      }

      const ymag = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2);
      if (ymag !== 0) {
        y0 /= ymag;
        y1 /= ymag;
        y2 /= ymag;
      }

      return {
        x: [x0, x1, x2],
        y: [y0, y1, y2],
        z: [z0, z1, z2]
      };
    }

    /**
     * Orbits the camera about center point. For use with orbitControl().
     * @private
     * @param {Number} dTheta change in spherical coordinate theta
     * @param {Number} dPhi change in spherical coordinate phi
     * @param {Number} dRadius change in radius
     */
    _orbit(dTheta, dPhi, dRadius) {
      // Calculate the vector and its magnitude from the center to the viewpoint
      const diffX = this.eyeX - this.centerX;
      const diffY = this.eyeY - this.centerY;
      const diffZ = this.eyeZ - this.centerZ;
      let camRadius = Math.hypot(diffX, diffY, diffZ);
      // front vector. unit vector from center to eye.
      const front = new Vector(diffX, diffY, diffZ).normalize();
      // up vector. normalized camera's up vector.
      const up = new Vector(this.upX, this.upY, this.upZ).normalize(); // y-axis
      // side vector. Right when viewed from the front
      const side = Vector.cross(up, front).normalize(); // x-axis
      // vertical vector. normalized vector of projection of front vector.
      const vertical = Vector.cross(side, up); // z-axis

      // update camRadius
      camRadius *= Math.pow(10, dRadius);
      // prevent zooming through the center:
      if (camRadius < this.cameraNear) {
        camRadius = this.cameraNear;
      }
      if (camRadius > this.cameraFar) {
        camRadius = this.cameraFar;
      }

      // calculate updated camera angle
      // Find the angle between the "up" and the "front", add dPhi to that.
      // angleBetween() may return negative value. Since this specification is subject to change
      // due to version updates, it cannot be adopted, so here we calculate using a method
      // that directly obtains the absolute value.
      const camPhi =
        Math.acos(Math.max(-1, Math.min(1, Vector.dot(front, up)))) + dPhi;
      // Rotate by dTheta in the shortest direction from "vertical" to "side"
      const camTheta = dTheta;

      // Invert camera's upX, upY, upZ if dPhi is below 0 or above PI
      if (camPhi <= 0 || camPhi >= Math.PI) {
        this.upX *= -1;
        this.upY *= -1;
        this.upZ *= -1;
      }

      // update eye vector by calculate new front vector
      up.mult(Math.cos(camPhi));
      vertical.mult(Math.cos(camTheta) * Math.sin(camPhi));
      side.mult(Math.sin(camTheta) * Math.sin(camPhi));

      front.set(up).add(vertical).add(side);

      this.eyeX = camRadius * front.x + this.centerX;
      this.eyeY = camRadius * front.y + this.centerY;
      this.eyeZ = camRadius * front.z + this.centerZ;

      // update camera
      this.camera(
        this.eyeX, this.eyeY, this.eyeZ,
        this.centerX, this.centerY, this.centerZ,
        this.upX, this.upY, this.upZ
      );
    }

    /**
     * Orbits the camera about center point. For use with orbitControl().
     * Unlike _orbit(), the direction of rotation always matches the direction of pointer movement.
     * @private
     * @param {Number} dx the x component of the rotation vector.
     * @param {Number} dy the y component of the rotation vector.
     * @param {Number} dRadius change in radius
     */
    _orbitFree(dx, dy, dRadius) {
      // Calculate the vector and its magnitude from the center to the viewpoint
      const diffX = this.eyeX - this.centerX;
      const diffY = this.eyeY - this.centerY;
      const diffZ = this.eyeZ - this.centerZ;
      let camRadius = Math.hypot(diffX, diffY, diffZ);
      // front vector. unit vector from center to eye.
      const front = new Vector(diffX, diffY, diffZ).normalize();
      // up vector. camera's up vector.
      const up = new Vector(this.upX, this.upY, this.upZ);
      // side vector. Right when viewed from the front. (like x-axis)
      const side = Vector.cross(up, front).normalize();
      // down vector. Bottom when viewed from the front. (like y-axis)
      const down = Vector.cross(front, side);

      // side vector and down vector are no longer used as-is.
      // Create a vector representing the direction of rotation
      // in the form cos(direction)*side + sin(direction)*down.
      // Make the current side vector into this.
      const directionAngle = Math.atan2(dy, dx);
      down.mult(Math.sin(directionAngle));
      side.mult(Math.cos(directionAngle)).add(down);
      // The amount of rotation is the size of the vector (dx, dy).
      const rotAngle = Math.sqrt(dx * dx + dy * dy);
      // The vector that is orthogonal to both the front vector and
      // the rotation direction vector is the rotation axis vector.
      const axis = Vector.cross(front, side);

      // update camRadius
      camRadius *= Math.pow(10, dRadius);
      // prevent zooming through the center:
      if (camRadius < this.cameraNear) {
        camRadius = this.cameraNear;
      }
      if (camRadius > this.cameraFar) {
        camRadius = this.cameraFar;
      }

      // If the axis vector is likened to the z-axis, the front vector is
      // the x-axis and the side vector is the y-axis. Rotate the up and front
      // vectors respectively by thinking of them as rotations around the z-axis.

      // Calculate the components by taking the dot product and
      // calculate a rotation based on that.
      const c = Math.cos(rotAngle);
      const s = Math.sin(rotAngle);
      const dotFront = up.dot(front);
      const dotSide = up.dot(side);
      const ux = dotFront * c + dotSide * s;
      const uy = -dotFront * s + dotSide * c;
      const uz = up.dot(axis);
      up.x = ux * front.x + uy * side.x + uz * axis.x;
      up.y = ux * front.y + uy * side.y + uz * axis.y;
      up.z = ux * front.z + uy * side.z + uz * axis.z;
      // We won't be using the side vector and the front vector anymore,
      // so let's make the front vector into the vector from the center to the new eye.
      side.mult(-s);
      front.mult(c).add(side).mult(camRadius);

      // it's complete. let's update camera.
      this.camera(
        front.x + this.centerX,
        front.y + this.centerY,
        front.z + this.centerZ,
        this.centerX, this.centerY, this.centerZ,
        up.x, up.y, up.z
      );
    }

    /**
     * Returns true if camera is currently attached to renderer.
     * @private
     */
    _isActive() {
      return this === this._renderer.states.curCamera;
    }
  }
  function camera(p5, fn){
    ////////////////////////////////////////////////////////////////////////////////
    // p5.Prototype Methods
    ////////////////////////////////////////////////////////////////////////////////

    /**
     * Sets the position and orientation of the current camera in a 3D sketch.
     *
     * `camera()` allows objects to be viewed from different angles. It has nine
     * parameters that are all optional.
     *
     * The first three parameters, `x`, `y`, and `z`, are the coordinates of the
     * camera’s position. For example, calling `camera(0, 0, 0)` places the camera
     * at the origin `(0, 0, 0)`. By default, the camera is placed at
     * `(0, 0, 800)`.
     *
     * The next three parameters, `centerX`, `centerY`, and `centerZ` are the
     * coordinates of the point where the camera faces. For example, calling
     * `camera(0, 0, 0, 10, 20, 30)` places the camera at the origin `(0, 0, 0)`
     * and points it at `(10, 20, 30)`. By default, the camera points at the
     * origin `(0, 0, 0)`.
     *
     * The last three parameters, `upX`, `upY`, and `upZ` are the components of
     * the "up" vector. The "up" vector orients the camera’s y-axis. For example,
     * calling `camera(0, 0, 0, 10, 20, 30, 0, -1, 0)` places the camera at the
     * origin `(0, 0, 0)`, points it at `(10, 20, 30)`, and sets the "up" vector
     * to `(0, -1, 0)` which is like holding it upside-down. By default, the "up"
     * vector is `(0, 1, 0)`.
     *
     * Note: `camera()` can only be used in WebGL mode.
     *
     * @method camera
     * @for p5
     * @param  {Number} [x]        x-coordinate of the camera. Defaults to 0.
     * @param  {Number} [y]        y-coordinate of the camera. Defaults to 0.
     * @param  {Number} [z]        z-coordinate of the camera. Defaults to 800.
     * @param  {Number} [centerX]  x-coordinate of the point the camera faces. Defaults to 0.
     * @param  {Number} [centerY]  y-coordinate of the point the camera faces. Defaults to 0.
     * @param  {Number} [centerZ]  z-coordinate of the point the camera faces. Defaults to 0.
     * @param  {Number} [upX]      x-component of the camera’s "up" vector. Defaults to 0.
     * @param  {Number} [upY]      y-component of the camera’s "up" vector. Defaults to 1.
     * @param  {Number} [upZ]      z-component of the camera’s "up" vector. Defaults to 0.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cube on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Move the camera to the top-right.
     *   camera(200, -400, 800);
     *
     *   // Draw the box.
     *   box();
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cube apperas to sway left and right on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the camera's x-coordinate.
     *   let x = 400 * cos(frameCount * 0.01);
     *
     *   // Orbit the camera around the box.
     *   camera(x, -400, 800);
     *
     *   // Draw the box.
     *   box();
     * }
     *
     * @example
     * // Adjust the range sliders to change the camera's position.
     *
     * let xSlider;
     * let ySlider;
     * let zSlider;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create slider objects to set the camera's coordinates.
     *   xSlider = createSlider(-400, 400, 400);
     *   xSlider.position(0, 100);
     *   xSlider.size(100);
     *   ySlider = createSlider(-400, 400, -200);
     *   ySlider.position(0, 120);
     *   ySlider.size(100);
     *   zSlider = createSlider(0, 1600, 800);
     *   zSlider.position(0, 140);
     *   zSlider.size(100);
     *
     *   describe(
     *     'A white cube drawn against a gray background. Three range sliders appear beneath the image. The camera position changes when the user moves the sliders.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Get the camera's coordinates from the sliders.
     *   let x = xSlider.value();
     *   let y = ySlider.value();
     *   let z = zSlider.value();
     *
     *   // Move the camera.
     *   camera(x, y, z);
     *
     *   // Draw the box.
     *   box();
     * }
     */
    fn.camera = function (...args) {
      this._assert3d('camera');
      // p5._validateParameters('camera', args);
      this._renderer.camera(...args);
      return this;
    };

    /**
     * Sets a perspective projection for the current camera in a 3D sketch.
     *
     * In a perspective projection, shapes that are further from the camera appear
     * smaller than shapes that are near the camera. This technique, called
     * foreshortening, creates realistic 3D scenes. It’s applied by default in
     * WebGL mode.
     *
     * `perspective()` changes the camera’s perspective by changing its viewing
     * frustum. The frustum is the volume of space that’s visible to the camera.
     * Its shape is a pyramid with its top cut off. The camera is placed where
     * the top of the pyramid should be and views everything between the frustum’s
     * top (near) plane and its bottom (far) plane.
     *
     * The first parameter, `fovy`, is the camera’s vertical field of view. It’s
     * an angle that describes how tall or narrow a view the camera has. For
     * example, calling `perspective(0.5)` sets the camera’s vertical field of
     * view to 0.5 radians. By default, `fovy` is calculated based on the sketch’s
     * height and the camera’s default z-coordinate, which is 800. The formula for
     * the default `fovy` is `2 * atan(height / 2 / 800)`.
     *
     * The second parameter, `aspect`, is the camera’s aspect ratio. It’s a number
     * that describes the ratio of the top plane’s width to its height. For
     * example, calling `perspective(0.5, 1.5)` sets the camera’s field of view to
     * 0.5 radians and aspect ratio to 1.5, which would make shapes appear thinner
     * on a square canvas. By default, aspect is set to `width / height`.
     *
     * The third parameter, `near`, is the distance from the camera to the near
     * plane. For example, calling `perspective(0.5, 1.5, 100)` sets the camera’s
     * field of view to 0.5 radians, its aspect ratio to 1.5, and places the near
     * plane 100 pixels from the camera. Any shapes drawn less than 100 pixels
     * from the camera won’t be visible. By default, near is set to `0.1 * 800`,
     * which is 1/10th the default distance between the camera and the origin.
     *
     * The fourth parameter, `far`, is the distance from the camera to the far
     * plane. For example, calling `perspective(0.5, 1.5, 100, 10000)` sets the
     * camera’s field of view to 0.5 radians, its aspect ratio to 1.5, places the
     * near plane 100 pixels from the camera, and places the far plane 10,000
     * pixels from the camera. Any shapes drawn more than 10,000 pixels from the
     * camera won’t be visible. By default, far is set to `10 * 800`, which is 10
     * times the default distance between the camera and the origin.
     *
     * Note: `perspective()` can only be used in WebGL mode.
     *
     * @method  perspective
     * @for p5
     * @param  {Number} [fovy]   camera frustum vertical field of view. Defaults to
     *                           `2 * atan(height / 2 / 800)`.
     * @param  {Number} [aspect] camera frustum aspect ratio. Defaults to
     *                           `width / height`.
     * @param  {Number} [near]   distance from the camera to the near clipping plane.
     *                           Defaults to `0.1 * 800`.
     * @param  {Number} [far]    distance from the camera to the far clipping plane.
     *                           Defaults to `10 * 800`.
     * @chainable
     *
     * @example
     * // Double-click to squeeze the box.
     *
     * let isSqueezed = false;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white rectangular prism on a gray background. The box appears to become thinner when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Place the camera at the top-right.
     *   camera(400, -400, 800);
     *
     *   if (isSqueezed === true) {
     *     // Set fovy to 0.2.
     *     // Set aspect to 1.5.
     *     perspective(0.2, 1.5);
     *   }
     *
     *   // Draw the box.
     *   box();
     * }
     *
     * // Change the camera's perspective when the user double-clicks.
     * function doubleClicked() {
     *   isSqueezed = true;
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white rectangular prism on a gray background. The prism moves away from the camera until it disappears.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Place the camera at the top-right.
     *   camera(400, -400, 800);
     *
     *   // Set fovy to 0.2.
     *   // Set aspect to 1.5.
     *   // Set near to 600.
     *   // Set far to 1200.
     *   perspective(0.2, 1.5, 600, 1200);
     *
     *   // Move the origin away from the camera.
     *   let x = -frameCount;
     *   let y = frameCount;
     *   let z = -2 * frameCount;
     *   translate(x, y, z);
     *
     *   // Draw the box.
     *   box();
     * }
     */
    fn.perspective = function (...args) {
      this._assert3d('perspective');
      // p5._validateParameters('perspective', args);
      this._renderer.perspective(...args);
      return this;
    };

    /**
     * Enables or disables perspective for lines in 3D sketches.
     *
     * In WebGL mode, lines can be drawn with a thinner stroke when they’re
     * further from the camera. Doing so gives them a more realistic appearance.
     *
     * By default, lines are drawn differently based on the type of perspective
     * being used:
     * - `perspective()` and `frustum()` simulate a realistic perspective. In
     * these modes, stroke weight is affected by the line’s distance from the
     * camera. Doing so results in a more natural appearance. `perspective()` is
     * the default mode for 3D sketches.
     * - `ortho()` doesn’t simulate a realistic perspective. In this mode, stroke
     * weights are consistent regardless of the line’s distance from the camera.
     * Doing so results in a more predictable and consistent appearance.
     *
     * `linePerspective()` can override the default line drawing mode.
     *
     * The parameter, `enable`, is optional. It’s a `Boolean` value that sets the
     * way lines are drawn. If `true` is passed, as in `linePerspective(true)`,
     * then lines will appear thinner when they are further from the camera. If
     * `false` is passed, as in `linePerspective(false)`, then lines will have
     * consistent stroke weights regardless of their distance from the camera. By
     * default, `linePerspective()` is enabled.
     *
     * Calling `linePerspective()` without passing an argument returns `true` if
     * it's enabled and `false` if not.
     *
     * Note: `linePerspective()` can only be used in WebGL mode.
     *
     * @method linePerspective
     * @for p5
     * @param {Boolean} enable whether to enable line perspective.
     *
     * @example
     * // Double-click the canvas to toggle the line perspective.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     'A white cube with black edges on a gray background. Its edges toggle between thick and thin when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Translate the origin toward the camera.
     *   translate(-10, 10, 600);
     *
     *   // Rotate the coordinate system.
     *   rotateY(-0.1);
     *   rotateX(-0.1);
     *
     *   // Draw the row of boxes.
     *   for (let i = 0; i < 6; i += 1) {
     *     translate(0, 0, -40);
     *     box(10);
     *   }
     * }
     *
     * // Toggle the line perspective when the user double-clicks.
     * function doubleClicked() {
     *   let isEnabled = linePerspective();
     *   linePerspective(!isEnabled);
     * }
     *
     * @example
     * // Double-click the canvas to toggle the line perspective.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     'A row of cubes with black edges on a gray background. Their edges toggle between thick and thin when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Use an orthographic projection.
     *   ortho();
     *
     *   // Translate the origin toward the camera.
     *   translate(-10, 10, 600);
     *
     *   // Rotate the coordinate system.
     *   rotateY(-0.1);
     *   rotateX(-0.1);
     *
     *   // Draw the row of boxes.
     *   for (let i = 0; i < 6; i += 1) {
     *     translate(0, 0, -40);
     *     box(10);
     *   }
     * }
     *
     * // Toggle the line perspective when the user double-clicks.
     * function doubleClicked() {
     *   let isEnabled = linePerspective();
     *   linePerspective(!isEnabled);
     * }
     */
    /**
     * @method linePerspective
     * @return {boolean} whether line perspective is enabled.
     */
    fn.linePerspective = function (enable) {
      // p5._validateParameters('linePerspective', arguments);
      if (!(this._renderer instanceof Renderer3D)) {
        throw new Error('linePerspective() must be called in WebGL mode.');
      }
      return this._renderer.linePerspective(enable);
    };


    /**
     * Sets an orthographic projection for the current camera in a 3D sketch.
     *
     * In an orthographic projection, shapes with the same size always appear the
     * same size, regardless of whether they are near or far from the camera.
     *
     * `ortho()` changes the camera’s perspective by changing its viewing frustum
     * from a truncated pyramid to a rectangular prism. The camera is placed in
     * front of the frustum and views everything between the frustum’s near plane
     * and its far plane. `ortho()` has six optional parameters to define the
     * frustum.
     *
     * The first four parameters, `left`, `right`, `bottom`, and `top`, set the
     * coordinates of the frustum’s sides, bottom, and top. For example, calling
     * `ortho(-100, 100, 200, -200)` creates a frustum that’s 200 pixels wide and
     * 400 pixels tall. By default, these coordinates are set based on the
     * sketch’s width and height, as in
     * `ortho(-width / 2, width / 2, -height / 2, height / 2)`.
     *
     * The last two parameters, `near` and `far`, set the distance of the
     * frustum’s near and far plane from the camera. For example, calling
     * `ortho(-100, 100, 200, 200, 50, 1000)` creates a frustum that’s 200 pixels
     * wide, 400 pixels tall, starts 50 pixels from the camera, and ends 1,000
     * pixels from the camera. By default, `near` and `far` are set to 0 and
     * `max(width, height) + 800`, respectively.
     *
     * Note: `ortho()` can only be used in WebGL mode.
     *
     * @method  ortho
     * @for p5
     * @param  {Number} [left]   x-coordinate of the frustum’s left plane. Defaults to `-width / 2`.
     * @param  {Number} [right]  x-coordinate of the frustum’s right plane. Defaults to `width / 2`.
     * @param  {Number} [bottom] y-coordinate of the frustum’s bottom plane. Defaults to `height / 2`.
     * @param  {Number} [top]    y-coordinate of the frustum’s top plane. Defaults to `-height / 2`.
     * @param  {Number} [near]   z-coordinate of the frustum’s near plane. Defaults to 0.
     * @param  {Number} [far]    z-coordinate of the frustum’s far plane. Defaults to `max(width, height) + 800`.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A row of tiny, white cubes on a gray background. All the cubes appear the same size.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Apply an orthographic projection.
     *   ortho();
     *
     *   // Translate the origin toward the camera.
     *   translate(-10, 10, 600);
     *
     *   // Rotate the coordinate system.
     *   rotateY(-0.1);
     *   rotateX(-0.1);
     *
     *   // Draw the row of boxes.
     *   for (let i = 0; i < 6; i += 1) {
     *     translate(0, 0, -40);
     *     box(10);
     *   }
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cube on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Apply an orthographic projection.
     *   // Center the frustum.
     *   // Set its width and height to 20.
     *   // Place its near plane 300 pixels from the camera.
     *   // Place its far plane 350 pixels from the camera.
     *   ortho(-10, 10, -10, 10, 300, 350);
     *
     *   // Translate the origin toward the camera.
     *   translate(-10, 10, 600);
     *
     *   // Rotate the coordinate system.
     *   rotateY(-0.1);
     *   rotateX(-0.1);
     *
     *   // Draw the row of boxes.
     *   for (let i = 0; i < 6; i += 1) {
     *     translate(0, 0, -40);
     *     box(10);
     *   }
     * }
     */
    fn.ortho = function (...args) {
      this._assert3d('ortho');
      // p5._validateParameters('ortho', args);
      this._renderer.ortho(...args);
      return this;
    };

    /**
     * Sets the frustum of the current camera in a 3D sketch.
     *
     * In a frustum projection, shapes that are further from the camera appear
     * smaller than shapes that are near the camera. This technique, called
     * foreshortening, creates realistic 3D scenes.
     *
     * `frustum()` changes the default camera’s perspective by changing its
     * viewing frustum. The frustum is the volume of space that’s visible to the
     * camera. The frustum’s shape is a pyramid with its top cut off. The camera
     * is placed where the top of the pyramid should be and points towards the
     * base of the pyramid. It views everything within the frustum.
     *
     * The first four parameters, `left`, `right`, `bottom`, and `top`, set the
     * coordinates of the frustum’s sides, bottom, and top. For example, calling
     * `frustum(-100, 100, 200, -200)` creates a frustum that’s 200 pixels wide
     * and 400 pixels tall. By default, these coordinates are set based on the
     * sketch’s width and height, as in
     * `ortho(-width / 20, width / 20, height / 20, -height / 20)`.
     *
     * The last two parameters, `near` and `far`, set the distance of the
     * frustum’s near and far plane from the camera. For example, calling
     * `ortho(-100, 100, 200, -200, 50, 1000)` creates a frustum that’s 200 pixels
     * wide, 400 pixels tall, starts 50 pixels from the camera, and ends 1,000
     * pixels from the camera. By default, near is set to `0.1 * 800`, which is
     * 1/10th the default distance between the camera and the origin. `far` is set
     * to `10 * 800`, which is 10 times the default distance between the camera
     * and the origin.
     *
     * Note: `frustum()` can only be used in WebGL mode.
     *
     * @method frustum
     * @for p5
     * @param  {Number} [left]   x-coordinate of the frustum’s left plane. Defaults to `-width / 20`.
     * @param  {Number} [right]  x-coordinate of the frustum’s right plane. Defaults to `width / 20`.
     * @param  {Number} [bottom] y-coordinate of the frustum’s bottom plane. Defaults to `height / 20`.
     * @param  {Number} [top]    y-coordinate of the frustum’s top plane. Defaults to `-height / 20`.
     * @param  {Number} [near]   z-coordinate of the frustum’s near plane. Defaults to `0.1 * 800`.
     * @param  {Number} [far]    z-coordinate of the frustum’s far plane. Defaults to `10 * 800`.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A row of white cubes on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Apply the default frustum projection.
     *   frustum();
     *
     *   // Translate the origin toward the camera.
     *   translate(-10, 10, 600);
     *
     *   // Rotate the coordinate system.
     *   rotateY(-0.1);
     *   rotateX(-0.1);
     *
     *   // Draw the row of boxes.
     *   for (let i = 0; i < 6; i += 1) {
     *     translate(0, 0, -40);
     *     box(10);
     *   }
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *   describe('A white cube on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Adjust the frustum.
     *   // Center it.
     *   // Set its width and height to 20 pixels.
     *   // Place its near plane 300 pixels from the camera.
     *   // Place its far plane 350 pixels from the camera.
     *   frustum(-10, 10, -10, 10, 300, 350);
     *
     *   // Translate the origin toward the camera.
     *   translate(-10, 10, 600);
     *
     *   // Rotate the coordinate system.
     *   rotateY(-0.1);
     *   rotateX(-0.1);
     *
     *   // Draw the row of boxes.
     *   for (let i = 0; i < 6; i += 1) {
     *     translate(0, 0, -40);
     *     box(10);
     *   }
     * }
     */
    fn.frustum = function (...args) {
      this._assert3d('frustum');
      // p5._validateParameters('frustum', args);
      this._renderer.frustum(...args);
      return this;
    };

    /**
     * Creates a new <a href="#/p5.Camera">p5.Camera</a> object.
     *
     * The new camera is initialized with a default position `(0, 0, 800)` and a
     * default perspective projection. Its properties can be controlled with
     * <a href="#/p5.Camera">p5.Camera</a> methods such as
     * `myCamera.lookAt(0, 0, 0)`.
     *
     * Note: Every 3D sketch starts with a default camera initialized.
     * This camera can be controlled with the functions
     * <a href="#/p5/camera">camera()</a>,
     * <a href="#/p5/perspective">perspective()</a>,
     * <a href="#/p5/ortho">ortho()</a>, and
     * <a href="#/p5/frustum">frustum()</a> if it's the only camera in the scene.
     *
     * Note: `createCamera()` can only be used in WebGL mode.
     *
     * @method createCamera
     * @return {p5.Camera} the new camera.
     * @for p5
     *
     * @example
     * // Double-click to toggle between cameras.
     *
     * let cam1;
     * let cam2;
     * let usingCam1 = true;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the first camera.
     *   // Keep its default settings.
     *   cam1 = createCamera();
     *
     *   // Create the second camera.
     *   // Place it at the top-left.
     *   // Point it at the origin.
     *   cam2 = createCamera();
     *   cam2.setPosition(400, -400, 800);
     *   cam2.lookAt(0, 0, 0);
     *
     *   // Set the current camera to cam1.
     *   setCamera(cam1);
     *
     *   describe('A white cube on a gray background. The camera toggles between frontal and aerial views when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw the box.
     *   box();
     * }
     *
     * // Toggle the current camera when the user double-clicks.
     * function doubleClicked() {
     *   if (usingCam1 === true) {
     *     setCamera(cam2);
     *     usingCam1 = false;
     *   } else {
     *     setCamera(cam1);
     *     usingCam1 = true;
     *   }
     * }
     */
    fn.createCamera = function () {
      this._assert3d('createCamera');

      return this._renderer.createCamera();
    };

    /**
     * Sets the current (active) camera of a 3D sketch.
     *
     * `setCamera()` allows for switching between multiple cameras created with
     * <a href="#/p5/createCamera">createCamera()</a>.
     *
     * Note: `setCamera()` can only be used in WebGL mode.
     *
     * @method setCamera
     * @param  {p5.Camera} cam camera that should be made active.
     * @for p5
     *
     * @example
     * // Double-click to toggle between cameras.
     *
     * let cam1;
     * let cam2;
     * let usingCam1 = true;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the first camera.
     *   // Keep its default settings.
     *   cam1 = createCamera();
     *
     *   // Create the second camera.
     *   // Place it at the top-left.
     *   // Point it at the origin.
     *   cam2 = createCamera();
     *   cam2.setPosition(400, -400, 800);
     *   cam2.lookAt(0, 0, 0);
     *
     *   // Set the current camera to cam1.
     *   setCamera(cam1);
     *
     *   describe('A white cube on a gray background. The camera toggles between frontal and aerial views when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw the box.
     *   box();
     * }
     *
     * // Toggle the current camera when the user double-clicks.
     * function doubleClicked() {
     *   if (usingCam1 === true) {
     *     setCamera(cam2);
     *     usingCam1 = false;
     *   } else {
     *     setCamera(cam1);
     *     usingCam1 = true;
     *   }
     * }
     */
    fn.setCamera = function (cam) {
      this._renderer.setCamera(cam);
    };

    /**
     * A class to describe a camera for viewing a 3D sketch.
     *
     * Each `p5.Camera` object represents a camera that views a section of 3D
     * space. It stores information about the camera’s position, orientation, and
     * projection.
     *
     * In WebGL mode, the default camera is a `p5.Camera` object that can be
     * controlled with the <a href="#/p5/camera">camera()</a>,
     * <a href="#/p5/perspective">perspective()</a>,
     * <a href="#/p5/ortho">ortho()</a>, and
     * <a href="#/p5/frustum">frustum()</a> functions. Additional cameras can be
     * created with <a href="#/p5/createCamera">createCamera()</a> and activated
     * with <a href="#/p5/setCamera">setCamera()</a>.
     *
     * Note: `p5.Camera`’s methods operate in two coordinate systems:
     * - The “world” coordinate system describes positions in terms of their
     * relationship to the origin along the x-, y-, and z-axes. For example,
     * calling `myCamera.setPosition()` places the camera in 3D space using
     * "world" coordinates.
     * - The "local" coordinate system describes positions from the camera's point
     * of view: left-right, up-down, and forward-backward. For example, calling
     * `myCamera.move()` moves the camera along its own axes.
     *
     * @class p5.Camera
     * @constructor
     * @param {RendererGL} rendererGL instance of WebGL renderer
     *
     * @example
     * let cam;
     * let delta = 0.001;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-center.
     *   cam.setPosition(0, -400, 800);
     *
     *   // Point the camera at the origin.
     *   cam.lookAt(0, 0, 0);
     *
     *   describe(
     *     'A white cube on a gray background. The cube goes in and out of view as the camera pans left and right.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Turn the camera left and right, called "panning".
     *   cam.pan(delta);
     *
     *   // Switch directions every 120 frames.
     *   if (frameCount % 120 === 0) {
     *     delta *= -1;
     *   }
     *
     *   // Draw the box.
     *   box();
     * }
     *
     * @example
     * // Double-click to toggle between cameras.
     *
     * let cam1;
     * let cam2;
     * let isDefaultCamera = true;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the first camera.
     *   // Keep its default settings.
     *   cam1 = createCamera();
     *
     *   // Create the second camera.
     *   // Place it at the top-left.
     *   // Point it at the origin.
     *   cam2 = createCamera();
     *   cam2.setPosition(400, -400, 800);
     *   cam2.lookAt(0, 0, 0);
     *
     *   // Set the current camera to cam1.
     *   setCamera(cam1);
     *
     *   describe(
     *     'A white cube on a gray background. The camera toggles between frontal and aerial views when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw the box.
     *   box();
     * }
     *
     * // Toggle the current camera when the user double-clicks.
     * function doubleClicked() {
     *   if (isDefaultCamera === true) {
     *     setCamera(cam2);
     *     isDefaultCamera = false;
     *   } else {
     *     setCamera(cam1);
     *     isDefaultCamera = true;
     *   }
     * }
     */
    p5.Camera = Camera;

    Renderer3D.prototype.camera = function(...args) {
      this.states.setValue('curCamera', this.states.curCamera.clone());
      this.states.curCamera.camera(...args);
    };

    Renderer3D.prototype.perspective = function(...args) {
      this.states.setValue('curCamera', this.states.curCamera.clone());
      this.states.curCamera.perspective(...args);
    };

    Renderer3D.prototype.linePerspective = function(enable) {
      if (enable !== undefined) {
        this.states.setValue('curCamera', this.states.curCamera.clone());
        // Set the line perspective if enable is provided
        this.states.curCamera.useLinePerspective = enable;
      } else {
        // If no argument is provided, return the current value
        return this.states.curCamera.useLinePerspective;
      }
    };

    Renderer3D.prototype.ortho = function(...args) {
      this.states.setValue('curCamera', this.states.curCamera.clone());
      this.states.curCamera.ortho(...args);
    };

    Renderer3D.prototype.frustum = function(...args) {
      this.states.setValue('curCamera', this.states.curCamera.clone());
      this.states.curCamera.frustum(...args);
    };

    Renderer3D.prototype.createCamera = function() {
      // compute default camera settings, then set a default camera
      const _cam = new Camera(this);
      _cam._computeCameraDefaultSettings();
      _cam._setDefaultCamera();

      return _cam;
    };

    Renderer3D.prototype.setCamera = function(cam) {
      this.states.setValue('curCamera', cam);

      // set the projection matrix (which is not normally updated each frame)
      this.states.setValue('uPMatrix', this.states.uPMatrix.clone());
      this.states.uPMatrix.set(cam.projMatrix);
      this.states.setValue('uViewMatrix', this.states.uViewMatrix.clone());
      this.states.uViewMatrix.set(cam.cameraMatrix);
    };

    /**
     * The camera’s x-coordinate.
     *
     * By default, the camera’s x-coordinate is set to 0 in "world" space.
     *
     * @property {Number} eyeX
     * @for p5.Camera
     * @readonly
     *
     * @example
     * let cam;
     * let font;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   font = await loadFont('assets/inconsolata.otf');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-center.
     *   cam.setPosition(0, -400, 800);
     *
     *   // Point the camera at the origin.
     *   cam.lookAt(0, 0, 0);
     *
     *   describe(
     *     'A white cube on a gray background. The text "eyeX: 0" is written in black beneath it.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the box.
     *   fill(255);
     *
     *   // Draw the box.
     *   box();
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Display the value of eyeX, rounded to the nearest integer.
     *   text(`eyeX: ${round(cam.eyeX)}`, 0, 45);
     * }
     *
     * @example
     * let cam;
     * let font;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   font = await loadFont('assets/inconsolata.otf');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Place the camera at the top-center.
     *   cam.setPosition(0, -400, 800);
     *
     *   // Point the camera at the origin.
     *   cam.lookAt(0, 0, 0);
     *
     *   describe(
     *     'A white cube on a gray background. The cube appears to move left and right as the camera moves. The text "eyeX: X" is written in black beneath the cube. X oscillates between -25 and 25.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the box.
     *   fill(255);
     *
     *   // Draw the box.
     *   box();
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Calculate the new x-coordinate.
     *   let x = 25 * sin(frameCount * 0.01);
     *
     *   // Set the camera's position.
     *   cam.setPosition(x, -400, 800);
     *
     *   // Display the value of eyeX, rounded to the nearest integer.
     *   text(`eyeX: ${round(cam.eyeX)}`, 0, 45);
     * }
     */

    /**
     * The camera’s y-coordinate.
     *
     * By default, the camera’s y-coordinate is set to 0 in "world" space.
     *
     * @property {Number} eyeY
     * @for p5.Camera
     * @readonly
     *
     * @example
     * let cam;
     * let font;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   font = await loadFont('assets/inconsolata.otf');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Place the camera at the top-center.
     *   cam.setPosition(0, -400, 800);
     *
     *   // Point the camera at the origin.
     *   cam.lookAt(0, 0, 0);
     *
     *   // Set the camera.
     *   setCamera(cam);
     *
     *   describe(
     *     'A white cube on a gray background. The text "eyeY: -400" is written in black beneath it.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the box.
     *   fill(255);
     *
     *   // Draw the box.
     *   box();
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Display the value of eyeY, rounded to the nearest integer.
     *   text(`eyeY: ${round(cam.eyeY)}`, 0, 45);
     * }
     *
     * @example
     * let cam;
     * let font;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   font = await loadFont('assets/inconsolata.otf');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-center.
     *   cam.setPosition(0, -400, 800);
     *
     *   // Point the camera at the origin.
     *   cam.lookAt(0, 0, 0);
     *
     *   describe(
     *     'A white cube on a gray background. The cube appears to move up and down as the camera moves. The text "eyeY: Y" is written in black beneath the cube. Y oscillates between -374 and -425.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the box.
     *   fill(255);
     *
     *   // Draw the box.
     *   box();
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Calculate the new y-coordinate.
     *   let y = 25 * sin(frameCount * 0.01) - 400;
     *
     *   // Set the camera's position.
     *   cam.setPosition(0, y, 800);
     *
     *   // Display the value of eyeY, rounded to the nearest integer.
     *   text(`eyeY: ${round(cam.eyeY)}`, 0, 45);
     * }
     */

    /**
     * The camera’s z-coordinate.
     *
     * By default, the camera’s z-coordinate is set to 800 in "world" space.
     *
     * @property {Number} eyeZ
     * @for p5.Camera
     * @readonly
     *
     * @example
     * let cam;
     * let font;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   font = await loadFont('assets/inconsolata.otf');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-center.
     *   cam.setPosition(0, -400, 800);
     *
     *   // Point the camera at the origin.
     *   cam.lookAt(0, 0, 0);
     *
     *   describe(
     *     'A white cube on a gray background. The text "eyeZ: 800" is written in black beneath it.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the box.
     *   fill(255);
     *
     *   // Draw the box.
     *   box();
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Display the value of eyeZ, rounded to the nearest integer.
     *   text(`eyeZ: ${round(cam.eyeZ)}`, 0, 45);
     * }
     *
     * @example
     * let cam;
     * let font;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   font = await loadFont('assets/inconsolata.otf');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-center.
     *   cam.setPosition(0, -400, 800);
     *
     *   // Point the camera at the origin.
     *   cam.lookAt(0, 0, 0);
     *
     *   describe(
     *     'A white cube on a gray background. The cube appears to move forward and back as the camera moves. The text "eyeZ: Z" is written in black beneath the cube. Z oscillates between 700 and 900.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the box.
     *   fill(255);
     *
     *   // Draw the box.
     *   box();
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Calculate the new z-coordinate.
     *   let z = 100 * sin(frameCount * 0.01) + 800;
     *
     *   // Set the camera's position.
     *   cam.setPosition(0, -400, z);
     *
     *   // Display the value of eyeZ, rounded to the nearest integer.
     *   text(`eyeZ: ${round(cam.eyeZ)}`, 0, 45);
     * }
     */

    /**
     * The x-coordinate of the place where the camera looks.
     *
     * By default, the camera looks at the origin `(0, 0, 0)` in "world" space, so
     * `myCamera.centerX` is 0.
     *
     * @property {Number} centerX
     * @for p5.Camera
     * @readonly
     *
     * @example
     * let cam;
     * let font;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   font = await loadFont('assets/inconsolata.otf');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-center.
     *   cam.setPosition(0, -400, 800);
     *
     *   // Point the camera at (10, 20, -30).
     *   cam.lookAt(10, 20, -30);
     *
     *   describe(
     *     'A white cube on a gray background. The text "centerX: 10" is written in black beneath it.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the box.
     *   fill(255);
     *
     *   // Draw the box.
     *   box();
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Display the value of centerX, rounded to the nearest integer.
     *   text(`centerX: ${round(cam.centerX)}`, 0, 45);
     * }
     *
     * @example
     * let cam;
     * let font;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   font = await loadFont('assets/inconsolata.otf');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-right.
     *   cam.setPosition(100, -400, 800);
     *
     *   // Point the camera at (10, 20, -30).
     *   cam.lookAt(10, 20, -30);
     *
     *   describe(
     *     'A white cube on a gray background. The cube appears to move left and right as the camera shifts its focus. The text "centerX: X" is written in black beneath the cube. X oscillates between -15 and 35.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the box.
     *   fill(255);
     *
     *   // Draw the box.
     *   box();
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Calculate the new x-coordinate.
     *   let x = 25 * sin(frameCount * 0.01) + 10;
     *
     *   // Point the camera.
     *   cam.lookAt(x, 20, -30);
     *
     *   // Display the value of centerX, rounded to the nearest integer.
     *   text(`centerX: ${round(cam.centerX)}`, 0, 45);
     * }
     */

    /**
     * The y-coordinate of the place where the camera looks.
     *
     * By default, the camera looks at the origin `(0, 0, 0)` in "world" space, so
     * `myCamera.centerY` is 0.
     *
     * @property {Number} centerY
     * @for p5.Camera
     * @readonly
     *
     * @example
     * let cam;
     * let font;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   font = await loadFont('assets/inconsolata.otf');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-center.
     *   cam.setPosition(0, -400, 800);
     *
     *   // Point the camera at (10, 20, -30).
     *   cam.lookAt(10, 20, -30);
     *
     *   describe(
     *     'A white cube on a gray background. The text "centerY: 20" is written in black beneath it.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the box.
     *   fill(255);
     *
     *   // Draw the box.
     *   box();
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Display the value of centerY, rounded to the nearest integer.
     *   text(`centerY: ${round(cam.centerY)}`, 0, 45);
     * }
     *
     * @example
     * let cam;
     * let font;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   font = await loadFont('assets/inconsolata.otf');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-right.
     *   cam.setPosition(100, -400, 800);
     *
     *   // Point the camera at (10, 20, -30).
     *   cam.lookAt(10, 20, -30);
     *
     *   describe(
     *     'A white cube on a gray background. The cube appears to move up and down as the camera shifts its focus. The text "centerY: Y" is written in black beneath the cube. Y oscillates between -5 and 45.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the box.
     *   fill(255);
     *
     *   // Draw the box.
     *   box();
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Calculate the new y-coordinate.
     *   let y = 25 * sin(frameCount * 0.01) + 20;
     *
     *   // Point the camera.
     *   cam.lookAt(10, y, -30);
     *
     *   // Display the value of centerY, rounded to the nearest integer.
     *   text(`centerY: ${round(cam.centerY)}`, 0, 45);
     * }
     */

    /**
     * The y-coordinate of the place where the camera looks.
     *
     * By default, the camera looks at the origin `(0, 0, 0)` in "world" space, so
     * `myCamera.centerZ` is 0.
     *
     * @property {Number} centerZ
     * @for p5.Camera
     * @readonly
     *
     * @example
     * let cam;
     * let font;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   font = await loadFont('assets/inconsolata.otf');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-center.
     *   cam.setPosition(0, -400, 800);
     *
     *   // Point the camera at (10, 20, -30).
     *   cam.lookAt(10, 20, -30);
     *
     *   describe(
     *     'A white cube on a gray background. The text "centerZ: -30" is written in black beneath it.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the box.
     *   fill(255);
     *
     *   // Draw the box.
     *   box();
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Display the value of centerZ, rounded to the nearest integer.
     *   text(`centerZ: ${round(cam.centerZ)}`, 0, 45);
     * }
     *
     * @example
     * let cam;
     * let font;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   font = await loadFont('assets/inconsolata.otf');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Place the camera at the top-right.
     *   cam.setPosition(100, -400, 800);
     *
     *   // Point the camera at (10, 20, -30).
     *   cam.lookAt(10, 20, -30);
     *
     *   describe(
     *     'A white cube on a gray background. The cube appears to move forward and back as the camera shifts its focus. The text "centerZ: Z" is written in black beneath the cube. Z oscillates between -55 and -25.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the box.
     *   fill(255);
     *
     *   // Draw the box.
     *   box();
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Calculate the new z-coordinate.
     *   let z = 25 * sin(frameCount * 0.01) - 30;
     *
     *   // Point the camera.
     *   cam.lookAt(10, 20, z);
     *
     *   // Display the value of centerZ, rounded to the nearest integer.
     *   text(`centerZ: ${round(cam.centerZ)}`, 0, 45);
     * }
     */

    /**
     * The x-component of the camera's "up" vector.
     *
     * The camera's "up" vector orients its y-axis. By default, the "up" vector is
     * `(0, 1, 0)`, so its x-component is 0 in "local" space.
     *
     * @property {Number} upX
     * @for p5.Camera
     * @readonly
     *
     * @example
     * let cam;
     * let font;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   font = await loadFont('assets/inconsolata.otf');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-right: (100, -400, 800)
     *   // Point it at the origin: (0, 0, 0)
     *   // Set its "up" vector: (0, 1, 0).
     *   cam.camera(100, -400, 800, 0, 0, 0, 0, 1, 0);
     *
     *   describe(
     *     'A white cube on a gray background. The text "upX: 0" is written in black beneath it.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the box.
     *   fill(255);
     *
     *   // Draw the box.
     *   box();
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Display the value of upX, rounded to the nearest tenth.
     *   text(`upX: ${round(cam.upX, 1)}`, 0, 45);
     * }
     *
     * @example
     * let cam;
     * let font;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   font = await loadFont('assets/inconsolata.otf');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-right: (100, -400, 800)
     *   // Point it at the origin: (0, 0, 0)
     *   // Set its "up" vector: (0, 1, 0).
     *   cam.camera(100, -400, 800, 0, 0, 0, 0, 1, 0);
     *
     *   describe(
     *     'A white cube on a gray background. The cube appears to rock back and forth. The text "upX: X" is written in black beneath it. X oscillates between -1 and 1.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the box.
     *   fill(255);
     *
     *   // Draw the box.
     *   box();
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Calculate the x-component.
     *   let x = sin(frameCount * 0.01);
     *
     *   // Update the camera's "up" vector.
     *   cam.camera(100, -400, 800, 0, 0, 0, x, 1, 0);
     *
     *   // Display the value of upX, rounded to the nearest tenth.
     *   text(`upX: ${round(cam.upX, 1)}`, 0, 45);
     * }
     */

    /**
     * The y-component of the camera's "up" vector.
     *
     * The camera's "up" vector orients its y-axis. By default, the "up" vector is
     * `(0, 1, 0)`, so its y-component is 1 in "local" space.
     *
     * @property {Number} upY
     * @for p5.Camera
     * @readonly
     *
     * @example
     * let cam;
     * let font;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   font = await loadFont('assets/inconsolata.otf');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-right: (100, -400, 800)
     *   // Point it at the origin: (0, 0, 0)
     *   // Set its "up" vector: (0, 1, 0).
     *   cam.camera(100, -400, 800, 0, 0, 0, 0, 1, 0);
     *
     *   describe(
     *     'A white cube on a gray background. The text "upY: 1" is written in black beneath it.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the box.
     *   fill(255);
     *
     *   // Draw the box.
     *   box();
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Display the value of upY, rounded to the nearest tenth.
     *   text(`upY: ${round(cam.upY, 1)}`, 0, 45);
     * }
     *
     * @example
     * let cam;
     * let font;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   font = await loadFont('assets/inconsolata.otf');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-right: (100, -400, 800)
     *   // Point it at the origin: (0, 0, 0)
     *   // Set its "up" vector: (0, 1, 0).
     *   cam.camera(100, -400, 800, 0, 0, 0, 0, 1, 0);
     *
     *   describe(
     *     'A white cube on a gray background. The cube flips upside-down periodically. The text "upY: Y" is written in black beneath it. Y oscillates between -1 and 1.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the box.
     *   fill(255);
     *
     *   // Draw the box.
     *   box();
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Calculate the y-component.
     *   let y = sin(frameCount * 0.01);
     *
     *   // Update the camera's "up" vector.
     *   cam.camera(100, -400, 800, 0, 0, 0, 0, y, 0);
     *
     *   // Display the value of upY, rounded to the nearest tenth.
     *   text(`upY: ${round(cam.upY, 1)}`, 0, 45);
     * }
     */

    /**
     * The z-component of the camera's "up" vector.
     *
     * The camera's "up" vector orients its y-axis. By default, the "up" vector is
     * `(0, 1, 0)`, so its z-component is 0 in "local" space.
     *
     * @property {Number} upZ
     * @for p5.Camera
     * @readonly
     *
     * @example
     * let cam;
     * let font;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   font = await loadFont('assets/inconsolata.otf');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-right: (100, -400, 800)
     *   // Point it at the origin: (0, 0, 0)
     *   // Set its "up" vector: (0, 1, 0).
     *   cam.camera(100, -400, 800, 0, 0, 0, 0, 1, 0);
     *
     *   describe(
     *     'A white cube on a gray background. The text "upZ: 0" is written in black beneath it.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the box.
     *   fill(255);
     *
     *   // Draw the box.
     *   box();
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Display the value of upZ, rounded to the nearest tenth.
     *   text(`upZ: ${round(cam.upZ, 1)}`, 0, 45);
     * }
     *
     * @example
     * let cam;
     * let font;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   font = await loadFont('assets/inconsolata.otf');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Camera object.
     *   cam = createCamera();
     *
     *   // Set the camera
     *   setCamera(cam);
     *
     *   // Place the camera at the top-right: (100, -400, 800)
     *   // Point it at the origin: (0, 0, 0)
     *   // Set its "up" vector: (0, 1, 0).
     *   cam.camera(100, -400, 800, 0, 0, 0, 0, 1, 0);
     *
     *   describe(
     *     'A white cube on a gray background. The cube appears to rock back and forth. The text "upZ: Z" is written in black beneath it. Z oscillates between -1 and 1.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the box.
     *   fill(255);
     *
     *   // Draw the box.
     *   box();
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Calculate the z-component.
     *   let z = sin(frameCount * 0.01);
     *
     *   // Update the camera's "up" vector.
     *   cam.camera(100, -400, 800, 0, 0, 0, 0, 1, z);
     *
     *   // Display the value of upZ, rounded to the nearest tenth.
     *   text(`upZ: ${round(cam.upZ, 1)}`, 0, 45);
     * }
     */
  }

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

  var libtess_min = {exports: {}};

  /*

   Copyright 2000, Silicon Graphics, Inc. All Rights Reserved.
   Copyright 2015, Google Inc. All Rights Reserved.

   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 including the dates of first publication and
   either this permission notice or a reference to http://oss.sgi.com/projects/FreeB/
   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
   SILICON GRAPHICS, INC. 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.

   Original Code. The Original Code is: OpenGL Sample Implementation,
   Version 1.2.1, released January 26, 2000, developed by Silicon Graphics,
   Inc. The Original Code is Copyright (c) 1991-2000 Silicon Graphics, Inc.
   Copyright in any portions created by third parties is as indicated
   elsewhere herein. All Rights Reserved.
  */

  (function (module) {
  var n;function t(a,b){return a.b===b.b&&a.a===b.a}function u(a,b){return a.b<b.b||a.b===b.b&&a.a<=b.a}function v(a,b,c){var d=b.b-a.b,e=c.b-b.b;return 0<d+e?d<e?b.a-a.a+d/(d+e)*(a.a-c.a):b.a-c.a+e/(d+e)*(c.a-a.a):0}function x(a,b,c){var d=b.b-a.b,e=c.b-b.b;return 0<d+e?(b.a-c.a)*d+(b.a-a.a)*e:0}function z(a,b){return a.a<b.a||a.a===b.a&&a.b<=b.b}function aa(a,b,c){var d=b.a-a.a,e=c.a-b.a;return 0<d+e?d<e?b.b-a.b+d/(d+e)*(a.b-c.b):b.b-c.b+e/(d+e)*(c.b-a.b):0}
  	function ba(a,b,c){var d=b.a-a.a,e=c.a-b.a;return 0<d+e?(b.b-c.b)*d+(b.b-a.b)*e:0}function ca(a){return u(a.b.a,a.a)}function da(a){return u(a.a,a.b.a)}function A(a,b,c,d){a=0>a?0:a;c=0>c?0:c;return a<=c?0===c?(b+d)/2:b+a/(a+c)*(d-b):d+c/(a+c)*(b-d)}function ea(a){var b=B(a.b);C(b,a.c);C(b.b,a.c);D(b,a.a);return b}function E(a,b){var c=false,d=false;a!==b&&(b.a!==a.a&&(d=true,F(b.a,a.a)),b.d!==a.d&&(c=true,G(b.d,a.d)),H(b,a),d||(C(b,a.a),a.a.c=a),c||(D(b,a.d),a.d.a=a));}function I(a){var b=a.b,c=false;a.d!==a.b.d&&(c=true,G(a.d,a.b.d));a.c===a?F(a.a,null):(a.b.d.a=J(a),a.a.c=a.c,H(a,J(a)),c||D(a,a.d));b.c===b?(F(b.a,null),G(b.d,null)):(a.d.a=J(b),b.a.c=b.c,H(b,J(b)));fa(a);}
  	function K(a){var b=B(a),c=b.b;H(b,a.e);b.a=a.b.a;C(c,b.a);b.d=c.d=a.d;b=b.b;H(a.b,J(a.b));H(a.b,b);a.b.a=b.a;b.b.a.c=b.b;b.b.d=a.b.d;b.f=a.f;b.b.f=a.b.f;return b}function L(a,b){var c=false,d=B(a),e=d.b;b.d!==a.d&&(c=true,G(b.d,a.d));H(d,a.e);H(e,b);d.a=a.b.a;e.a=b.a;d.d=e.d=a.d;a.d.a=e;c||D(d,a.d);return d}function B(a){var b=new M,c=new M,d=a.b.h;c.h=d;d.b.h=b;b.h=a;a.b.h=c;b.b=c;b.c=b;b.e=c;c.b=b;c.c=c;return c.e=b}function H(a,b){var c=a.c,d=b.c;c.b.e=b;d.b.e=a;a.c=d;b.c=c;}
  	function C(a,b){var c=b.f,d=new N(b,c);c.e=d;b.f=d;c=d.c=a;do c.a=d,c=c.c;while(c!==a)}function D(a,b){var c=b.d,d=new ga(b,c);c.b=d;b.d=d;d.a=a;d.c=b.c;c=a;do c.d=d,c=c.e;while(c!==a)}function fa(a){var b=a.h;a=a.b.h;b.b.h=a;a.b.h=b;}function F(a,b){var c=a.c,d=c;do d.a=b,d=d.c;while(d!==c);c=a.f;d=a.e;d.f=c;c.e=d;}function G(a,b){var c=a.a,d=c;do d.d=b,d=d.e;while(d!==c);c=a.d;d=a.b;d.d=c;c.b=d;}function ha(a){var b=0;Math.abs(a[1])>Math.abs(a[0])&&(b=1);Math.abs(a[2])>Math.abs(a[b])&&(b=2);return b}var O=4*1E150;function P(a,b){a.f+=b.f;a.b.f+=b.b.f;}function ia(a,b,c){a=a.a;b=b.a;c=c.a;if(b.b.a===a)return c.b.a===a?u(b.a,c.a)?0>=x(c.b.a,b.a,c.a):0<=x(b.b.a,c.a,b.a):0>=x(c.b.a,a,c.a);if(c.b.a===a)return 0<=x(b.b.a,a,b.a);b=v(b.b.a,a,b.a);a=v(c.b.a,a,c.a);return b>=a}function Q(a){a.a.i=null;var b=a.e;b.a.c=b.c;b.c.a=b.a;a.e=null;}function ja(a,b){I(a.a);a.c=false;a.a=b;b.i=a;}function ka(a){var b=a.a.a;do a=R(a);while(a.a.a===b);a.c&&(b=L(S(a).a.b,a.a.e),ja(a,b),a=R(a));return a}
  	function la(a,b,c){var d=new ma;d.a=c;d.e=na(a.f,b.e,d);return c.i=d}function oa(a,b){switch(a.s){case 100130:return 0!==(b&1);case 100131:return 0!==b;case 100132:return 0<b;case 100133:return 0>b;case 100134:return 2<=b||-2>=b}return  false}function pa(a){var b=a.a,c=b.d;c.c=a.d;c.a=b;Q(a);}function T(a,b,c){a=b;for(b=b.a;a!==c;){a.c=false;var d=S(a),e=d.a;if(e.a!==b.a){if(!d.c){pa(a);break}e=L(b.c.b,e.b);ja(d,e);}b.c!==e&&(E(J(e),e),E(b,e));pa(a);b=d.a;a=d;}return b}
  	function U(a,b,c,d,e,f){var g=true;do la(a,b,c.b),c=c.c;while(c!==d);for(null===e&&(e=S(b).a.b.c);;){d=S(b);c=d.a.b;if(c.a!==e.a)break;c.c!==e&&(E(J(c),c),E(J(e),c));d.f=b.f-c.f;d.d=oa(a,d.f);b.b=true;!g&&qa(a,b)&&(P(c,e),Q(b),I(e));g=false;b=d;e=c;}b.b=true;f&&ra(a,b);}function sa(a,b,c,d,e){var f=[b.g[0],b.g[1],b.g[2]];b.d=null;b.d=a.o?a.o(f,c,d,a.c)||null:null;null===b.d&&(e?a.n||(V(a,100156),a.n=true):b.d=c[0]);}
  	function ta(a,b,c){var d=[null,null,null,null];d[0]=b.a.d;d[1]=c.a.d;sa(a,b.a,d,[.5,.5,0,0],false);E(b,c);}function ua(a,b,c,d,e){var f=Math.abs(b.b-a.b)+Math.abs(b.a-a.a),g=Math.abs(c.b-a.b)+Math.abs(c.a-a.a),h=e+1;d[e]=.5*g/(f+g);d[h]=.5*f/(f+g);a.g[0]+=d[e]*b.g[0]+d[h]*c.g[0];a.g[1]+=d[e]*b.g[1]+d[h]*c.g[1];a.g[2]+=d[e]*b.g[2]+d[h]*c.g[2];}
  	function qa(a,b){var c=S(b),d=b.a,e=c.a;if(u(d.a,e.a)){if(0<x(e.b.a,d.a,e.a))return  false;if(!t(d.a,e.a))K(e.b),E(d,J(e)),b.b=c.b=true;else if(d.a!==e.a){var c=a.e,f=d.a.h;if(0<=f){var c=c.b,g=c.d,h=c.e,k=c.c,l=k[f];g[l]=g[c.a];k[g[l]]=l;l<=--c.a&&(1>=l?W(c,l):u(h[g[l>>1]],h[g[l]])?W(c,l):va(c,l));h[f]=null;k[f]=c.b;c.b=f;}else for(c.c[-(f+1)]=null;0<c.a&&null===c.c[c.d[c.a-1]];)--c.a;ta(a,J(e),d);}}else {if(0>x(d.b.a,e.a,d.a))return  false;R(b).b=b.b=true;K(d.b);E(J(e),d);}return  true}
  	function wa(a,b){var c=S(b),d=b.a,e=c.a,f=d.a,g=e.a,h=d.b.a,k=e.b.a,l=new N;if(f===g||Math.min(f.a,h.a)>Math.max(g.a,k.a))return  false;if(u(f,g)){if(0<x(k,f,g))return  false}else if(0>x(h,g,f))return  false;var r=h,p=f,q=k,y=g,m,w;u(r,p)||(m=r,r=p,p=m);u(q,y)||(m=q,q=y,y=m);u(r,q)||(m=r,r=q,q=m,m=p,p=y,y=m);u(q,p)?u(p,y)?(m=v(r,q,p),w=v(q,p,y),0>m+w&&(m=-m,w=-w),l.b=A(m,q.b,w,p.b)):(m=x(r,q,p),w=-x(r,y,p),0>m+w&&(m=-m,w=-w),l.b=A(m,q.b,w,y.b)):l.b=(q.b+p.b)/2;z(r,p)||(m=r,r=p,p=m);z(q,y)||
  	(m=q,q=y,y=m);z(r,q)||(m=r,r=q,q=m,m=p,p=y,y=m);z(q,p)?z(p,y)?(m=aa(r,q,p),w=aa(q,p,y),0>m+w&&(m=-m,w=-w),l.a=A(m,q.a,w,p.a)):(m=ba(r,q,p),w=-ba(r,y,p),0>m+w&&(m=-m,w=-w),l.a=A(m,q.a,w,y.a)):l.a=(q.a+p.a)/2;u(l,a.a)&&(l.b=a.a.b,l.a=a.a.a);r=u(f,g)?f:g;u(r,l)&&(l.b=r.b,l.a=r.a);if(t(l,f)||t(l,g))return qa(a,b),false;if(!t(h,a.a)&&0<=x(h,a.a,l)||!t(k,a.a)&&0>=x(k,a.a,l)){if(k===a.a)return K(d.b),E(e.b,d),b=ka(b),d=S(b).a,T(a,S(b),c),U(a,b,J(d),d,d,true),true;if(h===a.a){K(e.b);E(d.e,J(e));f=c=b;g=f.a.b.a;
  	do f=R(f);while(f.a.b.a===g);b=f;f=S(b).a.b.c;c.a=J(e);e=T(a,c,null);U(a,b,e.c,d.b.c,f,true);return  true}0<=x(h,a.a,l)&&(R(b).b=b.b=true,K(d.b),d.a.b=a.a.b,d.a.a=a.a.a);0>=x(k,a.a,l)&&(b.b=c.b=true,K(e.b),e.a.b=a.a.b,e.a.a=a.a.a);return  false}K(d.b);K(e.b);E(J(e),d);d.a.b=l.b;d.a.a=l.a;d.a.h=xa(a.e,d.a);d=d.a;e=[0,0,0,0];l=[f.d,h.d,g.d,k.d];d.g[0]=d.g[1]=d.g[2]=0;ua(d,f,h,e,0);ua(d,g,k,e,2);sa(a,d,l,e,true);R(b).b=b.b=c.b=true;return  false}
  	function ra(a,b){for(var c=S(b);;){for(;c.b;)b=c,c=S(c);if(!b.b&&(c=b,b=R(b),null===b||!b.b))break;b.b=false;var d=b.a,e=c.a,f;if(f=d.b.a!==e.b.a)a:{f=b;var g=S(f),h=f.a,k=g.a,l=void 0;if(u(h.b.a,k.b.a)){if(0>x(h.b.a,k.b.a,h.a)){f=false;break a}R(f).b=f.b=true;l=K(h);E(k.b,l);l.d.c=f.d;}else {if(0<x(k.b.a,h.b.a,k.a)){f=false;break a}f.b=g.b=true;l=K(k);E(h.e,k.b);l.b.d.c=f.d;}f=true;}f&&(c.c?(Q(c),I(e),c=S(b),e=c.a):b.c&&(Q(b),I(d),b=R(c),d=b.a));if(d.a!==e.a)if(d.b.a===e.b.a||b.c||c.c||d.b.a!==a.a&&e.b.a!==a.a)qa(a,
  	b);else if(wa(a,b))break;d.a===e.a&&d.b.a===e.b.a&&(P(e,d),Q(b),I(d),b=R(c));}}
  	function ya(a,b){a.a=b;for(var c=b.c;null===c.i;)if(c=c.c,c===b.c){var c=a,d=b,e=new ma;e.a=d.c.b;var f=c.f,g=f.a;do g=g.a;while(null!==g.b&&!f.c(f.b,e,g.b));var f=g.b,h=S(f),e=f.a,g=h.a;if(0===x(e.b.a,d,e.a))e=f.a,t(e.a,d)||t(e.b.a,d)||(K(e.b),f.c&&(I(e.c),f.c=false),E(d.c,e),ya(c,d));else {var k=u(g.b.a,e.b.a)?f:h,h=void 0;f.d||k.c?(k===f?h=L(d.c.b,e.e):h=L(g.b.c.b,d.c).b,k.c?ja(k,h):(e=c,f=la(c,f,h),f.f=R(f).f+f.a.f,f.d=oa(e,f.f)),ya(c,d)):U(c,f,d.c,d.c,null,true);}return}c=ka(c.i);e=S(c);f=e.a;e=T(a,
  	e,null);if(e.c===f){var f=e,e=f.c,g=S(c),h=c.a,k=g.a,l=false;h.b.a!==k.b.a&&wa(a,c);t(h.a,a.a)&&(E(J(e),h),c=ka(c),e=S(c).a,T(a,S(c),g),l=true);t(k.a,a.a)&&(E(f,J(k)),f=T(a,g,null),l=true);l?U(a,c,f.c,e,e,true):(u(k.a,h.a)?d=J(k):d=h,d=L(f.c.b,d),U(a,c,d,d.c,d.c,false),d.b.i.c=true,ra(a,c));}else U(a,c,e.c,f,f,true);}function za(a,b){var c=new ma,d=ea(a.b);d.a.b=O;d.a.a=b;d.b.a.b=-O;d.b.a.a=b;a.a=d.b.a;c.a=d;c.f=0;c.d=false;c.c=false;c.h=true;c.b=false;d=a.f;d=na(d,d.a,c);c.e=d;}function Aa(a){this.a=new Ba;this.b=a;this.c=ia;}function na(a,b,c){do b=b.c;while(null!==b.b&&!a.c(a.b,b.b,c));a=new Ba(c,b.a,b);b.a.c=a;return b.a=a}function Ba(a,b,c){this.b=a||null;this.a=b||this;this.c=c||this;}function X(){this.d=Y;this.p=this.b=this.q=null;this.j=[0,0,0];this.s=100130;this.n=false;this.o=this.a=this.e=this.f=null;this.m=false;this.c=this.r=this.i=this.k=this.l=this.h=null;}var Y=0;n=X.prototype;n.x=function(){Z(this,Y);};n.B=function(a,b){switch(a){case 100142:return;case 100140:switch(b){case 100130:case 100131:case 100132:case 100133:case 100134:this.s=b;return}break;case 100141:this.m=!!b;return;default:V(this,100900);return}V(this,100901);};
  	n.y=function(a){switch(a){case 100142:return 0;case 100140:return this.s;case 100141:return this.m;default:V(this,100900);}return  false};n.A=function(a,b,c){this.j[0]=a;this.j[1]=b;this.j[2]=c;};
  	n.z=function(a,b){var c=b?b:null;switch(a){case 100100:case 100106:this.h=c;break;case 100104:case 100110:this.l=c;break;case 100101:case 100107:this.k=c;break;case 100102:case 100108:this.i=c;break;case 100103:case 100109:this.p=c;break;case 100105:case 100111:this.o=c;break;case 100112:this.r=c;break;default:V(this,100900);}};
  	n.C=function(a,b){var c=false,d=[0,0,0];Z(this,2);for(var e=0;3>e;++e){var f=a[e];-1e150>f&&(f=-1e150,c=true);1E150<f&&(f=1E150,c=true);d[e]=f;}c&&V(this,100155);c=this.q;null===c?(c=ea(this.b),E(c,c.b)):(K(c),c=c.e);c.a.d=b;c.a.g[0]=d[0];c.a.g[1]=d[1];c.a.g[2]=d[2];c.f=1;c.b.f=-1;this.q=c;};n.u=function(a){Z(this,Y);this.d=1;this.b=new Ca;this.c=a;};n.t=function(){Z(this,1);this.d=2;this.q=null;};n.v=function(){Z(this,2);this.d=1;};
  	n.w=function(){Z(this,1);this.d=Y;var a=this.j[0],b=this.j[1],c=this.j[2],d=false,e=[a,b,c];if(0===a&&0===b&&0===c){for(var b=[-2*1E150,-2*1E150,-2*1E150],f=[2*1E150,2*1E150,2*1E150],c=[],g=[],d=this.b.c,a=d.e;a!==d;a=a.e)for(var h=0;3>h;++h){var k=a.g[h];k<f[h]&&(f[h]=k,g[h]=a);k>b[h]&&(b[h]=k,c[h]=a);}a=0;b[1]-f[1]>b[0]-f[0]&&(a=1);b[2]-f[2]>b[a]-f[a]&&(a=2);if(f[a]>=b[a])e[0]=0,e[1]=0,e[2]=1;else {b=0;f=g[a];c=c[a];g=[0,0,0];f=[f.g[0]-c.g[0],f.g[1]-c.g[1],f.g[2]-c.g[2]];h=[0,0,0];for(a=d.e;a!==d;a=
  	a.e)h[0]=a.g[0]-c.g[0],h[1]=a.g[1]-c.g[1],h[2]=a.g[2]-c.g[2],g[0]=f[1]*h[2]-f[2]*h[1],g[1]=f[2]*h[0]-f[0]*h[2],g[2]=f[0]*h[1]-f[1]*h[0],k=g[0]*g[0]+g[1]*g[1]+g[2]*g[2],k>b&&(b=k,e[0]=g[0],e[1]=g[1],e[2]=g[2]);0>=b&&(e[0]=e[1]=e[2]=0,e[ha(f)]=1);}d=true;}g=ha(e);a=this.b.c;b=(g+1)%3;c=(g+2)%3;g=0<e[g]?1:-1;for(e=a.e;e!==a;e=e.e)e.b=e.g[b],e.a=g*e.g[c];if(d){e=0;d=this.b.a;for(a=d.b;a!==d;a=a.b)if(b=a.a,!(0>=b.f)){do e+=(b.a.b-b.b.a.b)*(b.a.a+b.b.a.a),b=b.e;while(b!==a.a)}if(0>e)for(e=this.b.c,d=e.e;d!==
  	e;d=d.e)d.a=-d.a;}this.n=false;e=this.b.b;for(a=e.h;a!==e;a=d)if(d=a.h,b=a.e,t(a.a,a.b.a)&&a.e.e!==a&&(ta(this,b,a),I(a),a=b,b=a.e),b.e===a){if(b!==a){if(b===d||b===d.b)d=d.h;I(b);}if(a===d||a===d.b)d=d.h;I(a);}this.e=e=new Da;d=this.b.c;for(a=d.e;a!==d;a=a.e)a.h=xa(e,a);Ea(e);this.f=new Aa(this);za(this,-O);for(za(this,O);null!==(e=Fa(this.e));){for(;;){a:if(a=this.e,0===a.a)d=Ga(a.b);else if(d=a.c[a.d[a.a-1]],0!==a.b.a&&(a=Ga(a.b),u(a,d))){d=a;break a}if(null===d||!t(d,e))break;d=Fa(this.e);ta(this,e.c,
  	d.c);}ya(this,e);}this.a=this.f.a.a.b.a.a;for(e=0;null!==(d=this.f.a.a.b);)d.h||++e,Q(d);this.f=null;e=this.e;e.b=null;e.d=null;this.e=e.c=null;e=this.b;for(a=e.a.b;a!==e.a;a=d)d=a.b,a=a.a,a.e.e===a&&(P(a.c,a),I(a));if(!this.n){e=this.b;if(this.m)for(a=e.b.h;a!==e.b;a=d)d=a.h,a.b.d.c!==a.d.c?a.f=a.d.c?1:-1:I(a);else for(a=e.a.b;a!==e.a;a=d)if(d=a.b,a.c){for(a=a.a;u(a.b.a,a.a);a=a.c.b);for(;u(a.a,a.b.a);a=a.e);b=a.c.b;for(c=void 0;a.e!==b;)if(u(a.b.a,b.a)){for(;b.e!==a&&(ca(b.e)||0>=x(b.a,b.b.a,b.e.b.a));)c=
  	L(b.e,b),b=c.b;b=b.c.b;}else {for(;b.e!==a&&(da(a.c.b)||0<=x(a.b.a,a.a,a.c.b.a));)c=L(a,a.c.b),a=c.b;a=a.e;}for(;b.e.e!==a;)c=L(b.e,b),b=c.b;}if(this.h||this.i||this.k||this.l)if(this.m)for(e=this.b,d=e.a.b;d!==e.a;d=d.b){if(d.c){this.h&&this.h(2,this.c);a=d.a;do this.k&&this.k(a.a.d,this.c),a=a.e;while(a!==d.a);this.i&&this.i(this.c);}}else {e=this.b;d=!!this.l;a=false;b=-1;for(c=e.a.d;c!==e.a;c=c.d)if(c.c){a||(this.h&&this.h(4,this.c),a=true);g=c.a;do d&&(f=g.b.d.c?0:1,b!==f&&(b=f,this.l&&this.l(!!b,this.c))),
  	this.k&&this.k(g.a.d,this.c),g=g.e;while(g!==c.a)}a&&this.i&&this.i(this.c);}if(this.r){e=this.b;for(a=e.a.b;a!==e.a;a=d)if(d=a.b,!a.c){b=a.a;c=b.e;g=void 0;do g=c,c=g.e,g.d=null,null===g.b.d&&(g.c===g?F(g.a,null):(g.a.c=g.c,H(g,J(g))),f=g.b,f.c===f?F(f.a,null):(f.a.c=f.c,H(f,J(f))),fa(g));while(g!==b);b=a.d;a=a.b;a.d=b;b.b=a;}this.r(this.b);this.c=this.b=null;return}}this.b=this.c=null;};
  	function Z(a,b){if(a.d!==b)for(;a.d!==b;)if(a.d<b)switch(a.d){case Y:V(a,100151);a.u(null);break;case 1:V(a,100152),a.t();}else switch(a.d){case 2:V(a,100154);a.v();break;case 1:V(a,100153),a.w();}}function V(a,b){a.p&&a.p(b,a.c);}function ga(a,b){this.b=a||this;this.d=b||this;this.a=null;this.c=false;}function M(){this.h=this;this.i=this.d=this.a=this.e=this.c=this.b=null;this.f=0;}function J(a){return a.b.e}function Ca(){this.c=new N;this.a=new ga;this.b=new M;this.d=new M;this.b.b=this.d;this.d.b=this.b;}function N(a,b){this.e=a||this;this.f=b||this;this.d=this.c=null;this.g=[0,0,0];this.h=this.a=this.b=0;}function Da(){this.c=[];this.d=null;this.a=0;this.e=false;this.b=new Ha;}function Ea(a){a.d=[];for(var b=0;b<a.a;b++)a.d[b]=b;a.d.sort(function(a){return function(b,e){return u(a[b],a[e])?1:-1}}(a.c));a.e=true;Ia(a.b);}function xa(a,b){if(a.e){var c=a.b,d=++c.a;2*d>c.f&&(c.f*=2,c.c=Ja(c.c,c.f+1));var e;0===c.b?e=d:(e=c.b,c.b=c.c[c.b]);c.e[e]=b;c.c[e]=d;c.d[d]=e;c.h&&va(c,d);return e}c=a.a++;a.c[c]=b;return -(c+1)}
  	function Fa(a){if(0===a.a)return Ka(a.b);var b=a.c[a.d[a.a-1]];if(0!==a.b.a&&u(Ga(a.b),b))return Ka(a.b);do--a.a;while(0<a.a&&null===a.c[a.d[a.a-1]]);return b}function Ha(){this.d=Ja([0],33);this.e=[null,null];this.c=[0,0];this.a=0;this.f=32;this.b=0;this.h=false;this.d[1]=1;}function Ja(a,b){for(var c=Array(b),d=0;d<a.length;d++)c[d]=a[d];for(;d<b;d++)c[d]=0;return c}function Ia(a){for(var b=a.a;1<=b;--b)W(a,b);a.h=true;}function Ga(a){return a.e[a.d[1]]}function Ka(a){var b=a.d,c=a.e,d=a.c,e=b[1],f=c[e];0<a.a&&(b[1]=b[a.a],d[b[1]]=1,c[e]=null,d[e]=a.b,a.b=e,0<--a.a&&W(a,1));return f}
  	function W(a,b){for(var c=a.d,d=a.e,e=a.c,f=b,g=c[f];;){var h=f<<1;h<a.a&&u(d[c[h+1]],d[c[h]])&&(h+=1);var k=c[h];if(h>a.a||u(d[g],d[k])){c[f]=g;e[g]=f;break}c[f]=k;e[k]=f;f=h;}}function va(a,b){for(var c=a.d,d=a.e,e=a.c,f=b,g=c[f];;){var h=f>>1,k=c[h];if(0===h||u(d[k],d[g])){c[f]=g;e[g]=f;break}c[f]=k;e[k]=f;f=h;}}function ma(){this.e=this.a=null;this.f=0;this.c=this.b=this.h=this.d=false;}function S(a){return a.e.c.b}function R(a){return a.e.a.b}commonjsGlobal.libtess={GluTesselator:X,windingRule:{GLU_TESS_WINDING_ODD:100130,GLU_TESS_WINDING_NONZERO:100131,GLU_TESS_WINDING_POSITIVE:100132,GLU_TESS_WINDING_NEGATIVE:100133,GLU_TESS_WINDING_ABS_GEQ_TWO:100134},primitiveType:{GL_LINE_LOOP:2,GL_TRIANGLES:4,GL_TRIANGLE_STRIP:5,GL_TRIANGLE_FAN:6},errorType:{GLU_TESS_MISSING_BEGIN_POLYGON:100151,GLU_TESS_MISSING_END_POLYGON:100153,GLU_TESS_MISSING_BEGIN_CONTOUR:100152,GLU_TESS_MISSING_END_CONTOUR:100154,GLU_TESS_COORD_TOO_LARGE:100155,GLU_TESS_NEED_COMBINE_CALLBACK:100156},
  	gluEnum:{GLU_TESS_MESH:100112,GLU_TESS_TOLERANCE:100142,GLU_TESS_WINDING_RULE:100140,GLU_TESS_BOUNDARY_ONLY:100141,GLU_INVALID_ENUM:100900,GLU_INVALID_VALUE:100901,GLU_TESS_BEGIN:100100,GLU_TESS_VERTEX:100101,GLU_TESS_END:100102,GLU_TESS_ERROR:100103,GLU_TESS_EDGE_FLAG:100104,GLU_TESS_COMBINE:100105,GLU_TESS_BEGIN_DATA:100106,GLU_TESS_VERTEX_DATA:100107,GLU_TESS_END_DATA:100108,GLU_TESS_ERROR_DATA:100109,GLU_TESS_EDGE_FLAG_DATA:100110,GLU_TESS_COMBINE_DATA:100111}};X.prototype.gluDeleteTess=X.prototype.x;
  	X.prototype.gluTessProperty=X.prototype.B;X.prototype.gluGetTessProperty=X.prototype.y;X.prototype.gluTessNormal=X.prototype.A;X.prototype.gluTessCallback=X.prototype.z;X.prototype.gluTessVertex=X.prototype.C;X.prototype.gluTessBeginPolygon=X.prototype.u;X.prototype.gluTessBeginContour=X.prototype.t;X.prototype.gluTessEndContour=X.prototype.v;X.prototype.gluTessEndPolygon=X.prototype.w; { module.exports = commonjsGlobal.libtess; } 
  } (libtess_min));

  var libtess_minExports = libtess_min.exports;
  var libtess = /*@__PURE__*/getDefaultExportFromCjs(libtess_minExports);

  class RenderBuffer {
    constructor(size, src, dst, attr, renderer, map) {
      this.size = size; // the number of FLOATs in each vertex
      this.src = src; // the name of the model's source array
      this.dst = dst; // the name of the geometry's buffer
      this.attr = attr; // the name of the vertex attribute
      this._renderer = renderer;
      this.map = map; // optional, a transformation function to apply to src
    }

    default(cb) {
      this.default = cb;
      return this;
    }

    /**
     * Enables and binds the buffers used by shader when the appropriate data exists in geometry.
     * Must always be done prior to drawing geometry in WebGL.
     * @param {p5.Geometry} geometry Geometry that is going to be drawn
     * @param {p5.Shader} shader Active shader
     * @private
     */
    _prepareBuffer(geometry, shader) {
      this._renderer._prepareBuffer(this, geometry, shader);
    }
  }

  function renderBuffer(p5, fn) {
    p5.RenderBuffer = RenderBuffer;
  }

  if (typeof p5 !== 'undefined') {
    renderBuffer(p5);
  }

  const INITIAL_BUFFER_STRIDES = {
    vertices: 1,
    vertexNormals: 1,
    vertexColors: 4,
    vertexStrokeColors: 4,
    uvs: 2
  };

  // The total number of properties per vertex, before additional
  // user attributes are added.
  const INITIAL_VERTEX_SIZE =
    Object.values(INITIAL_BUFFER_STRIDES).reduce((acc, next) => acc + next);

  class ShapeBuilder {
    constructor(renderer) {
      this.renderer = renderer;
      this.shapeMode = PATH;
      this.geometry = new Geometry(
        undefined,
        undefined,
        undefined,
        this.renderer
      );
      this.geometry.gid = '__IMMEDIATE_MODE_GEOMETRY__';

      this.contourIndices = [];
      this._useUserVertexProperties = undefined;

      this._bezierVertex = [];
      this._quadraticVertex = [];
      this._curveVertex = [];

      // Used to distinguish between user calls to vertex() and internal calls
      this.isProcessingVertices = false;

      // Used for converting shape outlines into triangles for rendering
      this._tessy = this._initTessy();
      this.tessyVertexSize = INITIAL_VERTEX_SIZE;
      this.bufferStrides = { ...INITIAL_BUFFER_STRIDES };
    }

    constructFromContours(shape, contours) {
      if (this._useUserVertexProperties){
        this._resetUserVertexProperties();
      }
      this.geometry.reset();
      this.contourIndices = [];
      // TODO: handle just some contours having non-PATH mode
      this.shapeMode = shape.contours[0].kind;
      const shouldProcessEdges = !!this.renderer.states.strokeColor;

      const userVertexPropertyHelpers = {};
      if (shape.userVertexProperties) {
        this._useUserVertexProperties = true;
        for (const key in shape.userVertexProperties) {
          const name = shape.vertexPropertyName(key);
          const prop = this.geometry._userVertexPropertyHelper(
            name,
            [],
            shape.userVertexProperties[key]
          );
          userVertexPropertyHelpers[key] = prop;
          this.tessyVertexSize += prop.getDataSize();
          this.bufferStrides[prop.getSrcName()] = prop.getDataSize();
          this.renderer.buffers.user.push(
            new RenderBuffer(
              prop.getDataSize(),
              prop.getSrcName(),
              prop.getDstName(),
              name,
              this.renderer
            )
          );
        }
      } else {
        this._useUserVertexProperties = false;
      }

      for (const contour of contours) {
        this.contourIndices.push(this.geometry.vertices.length);
        for (const vertex of contour) {
          // WebGL doesn't support QUADS or QUAD_STRIP, so we duplicate data to turn
          // QUADS into TRIANGLES and QUAD_STRIP into TRIANGLE_STRIP. (There is no extra
          // work to convert QUAD_STRIP here, since the only difference is in how edges
          // are rendered.)
          if (this.shapeMode === QUADS) {
            // A finished quad turned into triangles should leave 6 vertices in the
            // buffer:
            // 0--3     0   3--5
            // |  | --> | \  \ |
            // 1--2     1--2   4
            // When vertex index 3 is being added, add the necessary duplicates.
            if (this.geometry.vertices.length % 6 === 3) {
              for (const key in this.bufferStrides) {
                const stride = this.bufferStrides[key];
                const buffer = this.geometry[key];
                buffer.push(
                  ...buffer.slice(
                    buffer.length - 3 * stride,
                    buffer.length - 2 * stride
                  ),
                  ...buffer.slice(buffer.length - stride, buffer.length)
                );
              }
            }
          }

          this.geometry.vertices.push(vertex.position);
          this.geometry.vertexNormals.push(vertex.normal || new Vector(0, 0, 0));
          this.geometry.uvs.push(
            vertex.textureCoordinates.x,
            vertex.textureCoordinates.y
          );
          if (this.renderer.states.fillColor) {
            this.geometry.vertexColors.push(...vertex.fill.array());
          } else {
            this.geometry.vertexColors.push(0, 0, 0, 0);
          }
          if (this.renderer.states.strokeColor) {
            this.geometry.vertexStrokeColors.push(...vertex.stroke.array());
          } else {
            this.geometry.vertexStrokeColors.push(0, 0, 0, 0);
          }
          for (const key in userVertexPropertyHelpers) {
            const prop = userVertexPropertyHelpers[key];
            if (key in vertex) {
              prop.setCurrentData(vertex[key]);
            }
            prop.pushCurrentData();
          }
        }
      }

      if (shouldProcessEdges) {
        this.geometry.edges = this._calculateEdges(
          this.shapeMode,
          this.geometry.vertices
        );
      }
      if (shouldProcessEdges && !this.renderer.geometryBuilder) {
        this.geometry._edgesToVertices();
      }

      if (this.shapeMode === PATH) {
        this.isProcessingVertices = true;
        this._tesselateShape();
        this.isProcessingVertices = false;
      } else if (this.shapeMode === QUAD_STRIP) {
        // The only difference between these two modes is which edges are
        // displayed, so after we've updated the edges, we switch the mode
        // to one that native WebGL knows how to render.
        this.shapeMode = TRIANGLE_STRIP;
      } else if (this.shapeMode === QUADS) {
        // We translate QUADS to TRIANGLES when vertices are being added,
        // since QUADS is just a p5 mode, whereas TRIANGLES is also a mode
        // that native WebGL knows how to render. Once we've processed edges,
        // everything should be set up for TRIANGLES mode.
        this.shapeMode = TRIANGLES;
      }

      if (
        this.renderer.states.textureMode === IMAGE &&
        this.renderer.states._tex !== null &&
        this.renderer.states._tex.width > 0 &&
        this.renderer.states._tex.height > 0
      ) {
        this.geometry.uvs = this.geometry.uvs.map((val, i) => {
          if (i % 2 === 0) {
            return val / this.renderer.states._tex.width;
          } else {
            return val / this.renderer.states._tex.height;
          }
        });
      }
    }

    _resetUserVertexProperties() {
      const properties = this.geometry.userVertexProperties;
      for (const propName in properties){
        const prop = properties[propName];
        delete this.bufferStrides[propName];
        prop.delete();
      }
      this._useUserVertexProperties = false;
      this.tessyVertexSize = INITIAL_VERTEX_SIZE;
      this.geometry.userVertexProperties = {};
    }

    /**
     * Called from _processVertices(). This function calculates the stroke vertices for custom shapes and
     * tesselates shapes when applicable.
     * @private
     * @returns  {Number[]} indices for custom shape vertices indicating edges.
     */
    _calculateEdges(
      shapeMode,
      verts
    ) {
      const res = [];
      let i = 0;
      const contourIndices = this.contourIndices.slice();
      let contourStart = -1;
      switch (shapeMode) {
        case TRIANGLE_STRIP:
          for (i = 0; i < verts.length - 2; i++) {
            res.push([i, i + 1]);
            res.push([i, i + 2]);
          }
          res.push([i, i + 1]);
          break;
        case TRIANGLE_FAN:
          for (i = 1; i < verts.length - 1; i++) {
            res.push([0, i]);
            res.push([i, i + 1]);
          }
          res.push([0, verts.length - 1]);
          break;
        case TRIANGLES:
          for (i = 0; i < verts.length - 2; i = i + 3) {
            res.push([i, i + 1]);
            res.push([i + 1, i + 2]);
            res.push([i + 2, i]);
          }
          break;
        case LINES:
          for (i = 0; i < verts.length - 1; i = i + 2) {
            res.push([i, i + 1]);
          }
          break;
        case QUADS:
          // Quads have been broken up into two triangles by `vertex()`:
          // 0   3--5
          // | \  \ |
          // 1--2   4
          for (i = 0; i < verts.length - 5; i += 6) {
            res.push([i, i + 1]);
            res.push([i + 1, i + 2]);
            res.push([i + 2, i + 5]);
            res.push([i + 5, i]);
          }
          break;
        case QUAD_STRIP:
          // 0---2---4
          // |   |   |
          // 1---3---5
          for (i = 0; i < verts.length - 2; i += 2) {
            res.push([i, i + 1]);
            res.push([i + 1, i + 3]);
            res.push([i, i + 2]);
          }
          res.push([i, i + 1]);
          break;
        default:
          // TODO: handle contours in other modes too
          for (i = 0; i < verts.length; i++) {
            if (i === contourIndices[0]) {
              contourStart = contourIndices.shift();
            } else if (
              verts[contourStart] &&
              verts[i].equals(verts[contourStart])
            ) {
              res.push([i - 1, contourStart]);
            } else {
              res.push([i - 1, i]);
            }
          }
          break;
      }
      return res;
    }

    /**
     * Called from _processVertices() when applicable. This function tesselates immediateMode.geometry.
     * @private
     */
    _tesselateShape() {
      // const contours = [[]];
      const contours = [];
      for (let i = 0; i < this.geometry.vertices.length; i++) {
        if (
          this.contourIndices.length > 0 &&
          this.contourIndices[0] === i
        ) {
          this.contourIndices.shift();
          contours.push([]);
        }
        contours[contours.length-1].push(
          this.geometry.vertices[i].x,
          this.geometry.vertices[i].y,
          this.geometry.vertices[i].z,
          this.geometry.uvs[i * 2],
          this.geometry.uvs[i * 2 + 1],
          this.geometry.vertexColors[i * 4],
          this.geometry.vertexColors[i * 4 + 1],
          this.geometry.vertexColors[i * 4 + 2],
          this.geometry.vertexColors[i * 4 + 3],
          this.geometry.vertexNormals[i].x,
          this.geometry.vertexNormals[i].y,
          this.geometry.vertexNormals[i].z
        );
        for (const propName in this.geometry.userVertexProperties) {
          const prop = this.geometry.userVertexProperties[propName];
          const start = i * prop.getDataSize();
          const end = start + prop.getDataSize();
          const vals = prop.getSrcArray().slice(start, end);
          contours[contours.length-1].push(...vals);
        }
      }

      // Normalize nearly identical consecutive vertices to prevent tessellation artifacts
      // This addresses numerical precision issues in libtess when consecutive vertices
      // have coordinates that are almost (but not exactly) equal (e.g., differing by ~1e-8)
      const epsilon = 1e-6;
      for (const contour of contours) {
        const stride = this.tessyVertexSize;
        for (let i = stride; i < contour.length; i += stride) {
          const prevX = contour[i - stride];
          const prevY = contour[i - stride + 1];
          const currX = contour[i];
          const currY = contour[i + 1];

          if (Math.abs(currX - prevX) < epsilon) {
            contour[i] = prevX;
          }
          if (Math.abs(currY - prevY) < epsilon) {
            contour[i + 1] = prevY;
          }
        }
      }

      const polyTriangles = this._triangulate(contours);

      // If there were no valid faces, we still want to use the original vertices
      // for strokes, so we'll stop here.
      if (polyTriangles.length === 0) {
        return;
      }

      // TODO: handle non-PATH shape modes that have contours
      this.shapeMode = TRIANGLES;
      const originalVertices = this.geometry.vertices;
      this.geometry.vertices = [];
      this.geometry.vertexNormals = [];
      this.geometry.uvs = [];
      for (const propName in this.geometry.userVertexProperties){
        const prop = this.geometry.userVertexProperties[propName];
        prop.resetSrcArray();
      }
      const colors = [];
      for (
        let j = 0, polyTriLength = polyTriangles.length;
        j < polyTriLength;
        j = j + this.tessyVertexSize
      ) {
        colors.push(...polyTriangles.slice(j + 5, j + 9));
        this.geometry.vertexNormals.push(
          new Vector(...polyTriangles.slice(j + 9, j + 12))
        );
        {
          let offset = 12;
          for (const propName in this.geometry.userVertexProperties){
            const prop = this.geometry.userVertexProperties[propName];
            const size = prop.getDataSize();
            const start = j + offset;
            const end = start + size;
            prop.setCurrentData(polyTriangles.slice(start, end));
            prop.pushCurrentData();
            offset += size;
          }
        }
        this.geometry.vertices.push(new Vector(...polyTriangles.slice(j, j + 3)));
        this.geometry.uvs.push(...polyTriangles.slice(j + 3, j + 5));
      }
      if (this.renderer.geometryBuilder) {
        // Tesselating the face causes the indices of edge vertices to stop being
        // correct. When rendering, this is not a problem, since _edgesToVertices
        // will have been called before this, and edge vertex indices are no longer
        // needed. However, the geometry builder still needs this information, so
        // when one is active, we need to update the indices.
        //
        // We record index mappings in a Map so that once we have found a
        // corresponding vertex, we don't need to loop to find it again.
        const newIndex = new Map();
        this.geometry.edges =
          this.geometry.edges.map(edge => edge.map(origIdx => {
            if (!newIndex.has(origIdx)) {
              const orig = originalVertices[origIdx];
              let newVertIndex = this.geometry.vertices.findIndex(
                v =>
                  orig.x === v.x &&
                  orig.y === v.y &&
                  orig.z === v.z
              );
              if (newVertIndex === -1) {
                // The tesselation process didn't output a vertex with the exact
                // coordinate as before, potentially due to numerical issues. This
                // doesn't happen often, but in this case, pick the closest point
                let closestDist = Infinity;
                let closestIndex = 0;
                for (
                  let i = 0;
                  i < this.geometry.vertices.length;
                  i++
                ) {
                  const vert = this.geometry.vertices[i];
                  const dX = orig.x - vert.x;
                  const dY = orig.y - vert.y;
                  const dZ = orig.z - vert.z;
                  const dist = dX*dX + dY*dY + dZ*dZ;
                  if (dist < closestDist) {
                    closestDist = dist;
                    closestIndex = i;
                  }
                }
                newVertIndex = closestIndex;
              }
              newIndex.set(origIdx, newVertIndex);
            }
            return newIndex.get(origIdx);
          }));
      }
      this.geometry.vertexColors = colors;
    }

    _initTessy() {
      // function called for each vertex of tesselator output
      function vertexCallback(data, polyVertArray) {
        for (const element of data) {
          polyVertArray.push(element);
        }
      }

      function begincallback(type) {
        if (type !== libtess.primitiveType.GL_TRIANGLES) {
          console.log(`expected TRIANGLES but got type: ${type}`);
        }
      }

      function errorcallback(errno) {
        console.log('error callback');
        console.log(`error number: ${errno}`);
      }

      // callback for when segments intersect and must be split
      const combinecallback = (coords, data, weight) => {
        const result = new Array(this.tessyVertexSize).fill(0);
        for (let i = 0; i < weight.length; i++) {
          for (let j = 0; j < result.length; j++) {
            if (weight[i] === 0 || !data[i]) continue;
            result[j] += data[i][j] * weight[i];
          }
        }
        return result;
      };

      function edgeCallback(flag) {
        // don't really care about the flag, but need no-strip/no-fan behavior
      }

      const tessy = new libtess.GluTesselator();
      tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, vertexCallback);
      tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_BEGIN, begincallback);
      tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR, errorcallback);
      tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback);
      tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, edgeCallback);
      tessy.gluTessProperty(
        libtess.gluEnum.GLU_TESS_WINDING_RULE,
        libtess.windingRule.GLU_TESS_WINDING_NONZERO
      );

      return tessy;
    }

    /**
     * Runs vertices through libtess to convert them into triangles
     * @private
     */
    _triangulate(contours) {
      // libtess will take 3d verts and flatten to a plane for tesselation.
      // libtess is capable of calculating a plane to tesselate on, but
      // if all of the vertices have the same z values, we'll just
      // assume the face is facing the camera, letting us skip any performance
      // issues or bugs in libtess's automatic calculation.
      const z = contours[0] ? contours[0][2] : undefined;
      let allSameZ = true;
      for (const contour of contours) {
        for (
          let j = 0;
          j < contour.length;
          j += this.tessyVertexSize
        ) {
          if (contour[j + 2] !== z) {
            allSameZ = false;
            break;
          }
        }
      }
      if (allSameZ) {
        this._tessy.gluTessNormal(0, 0, 1);
      } else {
        // Let libtess pick a plane for us
        this._tessy.gluTessNormal(0, 0, 0);
      }

      const triangleVerts = [];
      this._tessy.gluTessBeginPolygon(triangleVerts);

      for (const contour of contours) {
        this._tessy.gluTessBeginContour();
        for (
          let j = 0;
          j < contour.length;
          j += this.tessyVertexSize
        ) {
          const coords = contour.slice(
            j,
            j + this.tessyVertexSize
          );
          this._tessy.gluTessVertex(coords, coords);
        }
        this._tessy.gluTessEndContour();
      }

      // finish polygon
      this._tessy.gluTessEndPolygon();

      return triangleVerts;
    }
  }

  class GeometryBufferCache {
    constructor(renderer) {
      this.renderer = renderer;
      this.cache = {};
    }

    numCached() {
      return Object.keys(this.cache).length;
    }

    isCached(gid) {
      return this.cache[gid] !== undefined;
    }

    getGeometryByID(gid) {
      return this.cache[gid]?.geometry;
    }

    getCached(model) {
      return this.getCachedID(model.gid);
    }

    getCachedID(gid) {
      return this.cache[gid];
    }

    ensureCached(geometry) {
      const gid = geometry.gid;
      if (!gid) {
        throw new Error('The p5.Geometry you passed in has no gid property!');
      }

      if (this.isCached(gid)) return this.getCached(geometry);

      // Cache maintenance
      this.freeBuffers(gid);
      if (Object.keys(this.cache).length > 1000) {
        const key = Object.keys(this.cache)[0];
        this.freeBuffers(key);
      }

      const buffers = { geometry };
      this.cache[gid] = buffers;

      const indices = geometry.faces.length ? geometry.faces.flat() : null;

      // Determine index buffer type
      let indexType = null;
      if (indices) {
        // If any face references a vertex with an index greater than the maximum
        // un-singed 16 bit integer, then we need to use a Uint32Array instead of a
        // Uint16Array
        const hasVertexIndicesOverMaxUInt16 = indices.some(i => i > 65535);
        indexType = hasVertexIndicesOverMaxUInt16 ? Uint32Array : Uint16Array;
      }

      this.renderer._ensureGeometryBuffers(buffers, indices, indexType);

      return buffers;
    }

    freeBuffers(gid) {
      const buffers = this.cache[gid];
      if (!buffers) {
        return;
      }

      delete this.cache[gid];

      this.renderer._freeBuffers(buffers);
    }
  }

  const filterParamDefaults = {
    [BLUR]: 3,
    [POSTERIZE]: 4,
    [THRESHOLD]: 0.5
  };

  /**
   * @module Typography
   * @requires core
   */


  const DefaultFill = '#000000';

  const textCoreConstants = {
    IDEOGRAPHIC: 'ideographic',
    _CTX_MIDDLE: 'middle',
    _TEXT_BOUNDS: '_textBoundsSingle',
    _FONT_BOUNDS: '_fontBoundsSingle',
    HANGING: 'hanging',
    START: 'start',
    END: 'end'
  };

  function textCore(p5, fn) {
    const LeadingScale = 1.275;
    const LinebreakRe = /\r?\n/g;
    const CommaDelimRe = /,\s+/;
    const QuotedRe = /^".*"$/;
    const SpecialCharRe = /[^\x00-\x7F]/; // Non-ascii
    const TabsRe = /\t/g;

    const FontVariationSettings = 'fontVariationSettings';
    const VariableAxes = ['wght', 'wdth', 'ital', 'slnt', 'opsz'];
    const VariableAxesRe = new RegExp(`(?:${VariableAxes.join('|')})`);

    const textFunctions = [
      'text',
      'textAlign',
      'textAscent',
      'textDescent',
      'textLeading',
      'textMode',
      'textFont',
      'textSize',
      'textStyle',
      'textWidth',
      'textWrap',
      'textBounds',
      'textDirection',
      'textProperty',
      'textProperties',
      'fontBounds',
      'fontWidth',
      'fontAscent',
      'fontDescent',
      'textWeight'
    ];

    /**
     * Draws text to the canvas.
     *
     * The first parameter, `str`, is the text to be drawn. The second and third
     * parameters, `x` and `y`, set the coordinates of the text's bottom-left
     * corner. See <a href="#/p5/textAlign">textAlign()</a> for other ways to
     * align text.
     *
     * The fourth and fifth parameters, `maxWidth` and `maxHeight`, are optional.
     * They set the dimensions of the invisible rectangle containing the text. By
     * default, they set its  maximum width and height. See
     * <a href="#/p5/rectMode">rectMode()</a> for other ways to define the
     * rectangular text box. Text will wrap to fit within the text box. Text
     * outside of the box won't be drawn.
     *
     * Text can be styled a few ways. Call the <a href="#/p5/fill">fill()</a>
     * function to set the text's fill color. Call
     * <a href="#/p5/stroke">stroke()</a> and
     * <a href="#/p5/strokeWeight">strokeWeight()</a> to set the text's outline.
     * Call <a href="#/p5/textSize">textSize()</a> and
     * <a href="#/p5/textFont">textFont()</a> to set the text's size and font,
     * respectively.
     *
     * Note: `WEBGL` mode only supports fonts loaded with
     * <a href="#/p5/loadFont">loadFont()</a>. Calling
     * <a href="#/p5/stroke">stroke()</a> has no effect in `WEBGL` mode.
     *
     * @method text
     * @param {String|Object|Array|Number|Boolean} str text to be displayed.
     * @param {Number} x          x-coordinate of the text box.
     * @param {Number} y          y-coordinate of the text box.
     * @param {Number} [maxWidth] maximum width of the text box. See
     *                            <a href="#/p5/rectMode">rectMode()</a> for
     *                            other options.
     * @param {Number} [maxHeight] maximum height of the text box. See
     *                            <a href="#/p5/rectMode">rectMode()</a> for
     *                            other options.
     *
     * @for p5
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *   background(200);
     *   text('hi', 50, 50);
     *
     *   describe('The text "hi" written in black in the middle of a gray square.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *   background('skyblue');
     *   textSize(100);
     *   text('🌈', 0, 100);
     *
     *   describe('A rainbow in a blue sky.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *   textSize(32);
     *   fill(255);
     *   stroke(0);
     *   strokeWeight(4);
     *   text('hi', 50, 50);
     *
     *   describe('The text "hi" written in white with a black outline.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *   background('black');
     *   textSize(22);
     *   fill('yellow');
     *   text('rainbows', 6, 20);
     *   fill('cornflowerblue');
     *   text('rainbows', 6, 45);
     *   fill('tomato');
     *   text('rainbows', 6, 70);
     *   fill('limegreen');
     *   text('rainbows', 6, 95);
     *
     *   describe('The text "rainbows" written on several lines, each in a different color.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *   background(200);
     *   let s = 'The quick brown fox jumps over the lazy dog.';
     *   text(s, 10, 10, 70, 80);
     *
     *   describe('The sample text "The quick brown fox..." written in black across several lines.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *   background(200);
     *   rectMode(CENTER);
     *   let s = 'The quick brown fox jumps over the lazy dog.';
     *   text(s, 50, 50, 70, 80);
     *
     *   describe('The sample text "The quick brown fox..." written in black across several lines.');
     * }
     *
     * @example
     * let font;
     *
     * async function setup() {
     *   createCanvas(100, 100, WEBGL);
     *   font = await loadFont('assets/inconsolata.otf');
     *   textFont(font);
     *   textSize(32);
     *   textAlign(CENTER, CENTER);
     * }
     *
     * function draw() {
     *   background(200);
     *   rotateY(frameCount / 30);
     *   text('p5*js', 0, 0);
     *
     *   describe('The text "p5*js" written in white and spinning in 3D.');
     * }
     */

    /**
     * Sets the way text is aligned when <a href="#/p5/text">text()</a> is called.
     *
     * By default, calling `text('hi', 10, 20)` places the bottom-left corner of
     * the text's bounding box at (10, 20).
     *
     * The first parameter, `horizAlign`, changes the way
     * <a href="#/p5/text">text()</a> interprets x-coordinates. By default, the
     * x-coordinate sets the left edge of the bounding box. `textAlign()` accepts
     * the following values for `horizAlign`: `LEFT`, `CENTER`, or `RIGHT`.
     *
     * The second parameter, `vertAlign`, is optional. It changes the way
     * <a href="#/p5/text">text()</a> interprets y-coordinates. By default, the
     * y-coordinate sets the bottom edge of the bounding box. `textAlign()`
     * accepts the following values for `vertAlign`: `TOP`, `BOTTOM`, `CENTER`,
     * or `BASELINE`.
     *
     * Calling `textAlign()` without arguments returns the current alignment settings.
     *
     * @method textAlign
     * @for p5
     * @param {LEFT|CENTER|RIGHT} [horizAlign] horizontal alignment
     * @param {TOP|BOTTOM|CENTER|BASELINE} [vertAlign] vertical alignment
     * @returns {Object} If no arguments are provided, returns an object with current horizontal and vertical alignment
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Draw a vertical line.
     *   strokeWeight(0.5);
     *   line(50, 0, 50, 100);
     *
     *   // Top line.
     *   textSize(16);
     *   textAlign(RIGHT);
     *   text('ABCD', 50, 30);
     *
     *   // Middle line.
     *   textAlign(CENTER);
     *   text('EFGH', 50, 50);
     *
     *   // Bottom line.
     *   textAlign(LEFT);
     *   text('IJKL', 50, 70);
     *
     *   describe('The letters ABCD displayed at top-left, EFGH at center, and IJKL at bottom-right. A vertical line divides the canvas in half.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   strokeWeight(0.5);
     *
     *   // First line.
     *   line(0, 12, width, 12);
     *   textAlign(CENTER, TOP);
     *   text('TOP', 50, 12);
     *
     *   // Second line.
     *   line(0, 37, width, 37);
     *   textAlign(CENTER, CENTER);
     *   text('CENTER', 50, 37);
     *
     *   // Third line.
     *   line(0, 62, width, 62);
     *   textAlign(CENTER, BASELINE);
     *   text('BASELINE', 50, 62);
     *
     *   // Fourth line.
     *   line(0, 97, width, 97);
     *   textAlign(CENTER, BOTTOM);
     *   text('BOTTOM', 50, 97);
     *
     *   describe('The words "TOP", "CENTER", "BASELINE", and "BOTTOM" each drawn relative to a horizontal line. Their positions demonstrate different vertical alignments.');
     * }
     */

    /**
     * Returns the ascent of the text.
     *
     * The `textAscent()` function calculates the distance from the baseline to the
     * highest point of the current font. This value represents the ascent, which is essential
     * for determining the overall height of the text along with `textDescent()`. If
     * a text string is provided as an argument, the ascent is calculated based on that specific
     * string; otherwise, the ascent of the current font is returned.
     *
     * @method textAscent
     * @for p5
     *
     * @param {String} [txt] - (Optional) The text string for which to calculate the ascent.
     *                         If omitted, the function returns the ascent for the current font.
     * @returns {Number} The ascent value in pixels.
     *
     * @example
     * function setup() {
     *   createCanvas(400, 300);
     *   background(220);
     *
     *   textSize(48);
     *   textAlign(LEFT, BASELINE);
     *   textFont('Georgia');
     *
     *   let s = "Hello, p5.js!";
     *   let x = 50, y = 150;
     *
     *   fill(0);
     *   text(s, x, y);
     *
     *   // Get the ascent of the current font
     *   let asc = textAscent();
     *
     *   // Draw a red line at the baseline and a blue line at the ascent position
     *   stroke('red');
     *   line(x, y, x + 200, y); // Baseline
     *   stroke('blue');
     *   line(x, y - asc, x + 200, y - asc); // Ascent (top of text)
     *
     *   noStroke();
     *   fill(0);
     *   textSize(16);
     *   text("textAscent: " + asc.toFixed(2) + " pixels", x, y - asc - 10);
     * }
     *
     * @example
     * let font;
     *
     * async function setup()  {
     *   font = await loadFont('assets/inconsolata.otf');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textFont(font);
     *
     *   // Different for each font.
     *   let fontScale = 0.8;
     *
     *   let baseY = 75;
     *   strokeWeight(0.5);
     *
     *   // Draw small text.
     *   textSize(24);
     *   text('dp', 0, baseY);
     *
     *   // Draw baseline and ascent.
     *   let a = textAscent() * fontScale;
     *   line(0, baseY, 23, baseY);
     *   line(23, baseY - a, 23, baseY);
     *
     *   // Draw large text.
     *   textSize(48);
     *   text('dp', 45, baseY);
     *
     *   // Draw baseline and ascent.
     *   a = textAscent() * fontScale;
     *   line(45, baseY, 91, baseY);
     *   line(91, baseY - a, 91, baseY);
     *
     *   describe('The letters "dp" written twice in different sizes. Each version has a horizontal baseline. A vertical line extends upward from each baseline to the top of the "d".');
     * }
     */

    /**
     * Returns the descent of the text.
     *
     * The `textDescent()` function calculates the distance from the baseline to the
     * lowest point of the current font. This value represents the descent, which, when combined
     * with the ascent (from `textAscent()`), determines the overall vertical span of the text.
     * If a text string is provided as an argument, the descent is calculated based on that specific string;
     * otherwise, the descent of the current font is returned.
     *
     * @method textDescent
     * @for p5
     *
     * @param {String} [txt] - (Optional) The text string for which to calculate the descent.
     *                         If omitted, the function returns the descent for the current font.
     * @returns {Number} The descent value in pixels.
     *
     * @example
     * function setup() {
     *   createCanvas(400, 300);
     *   background(220);
     *
     *   textSize(48);
     *   textAlign(LEFT, BASELINE);
     *   textFont('Georgia');
     *
     *   let s = "Hello, p5.js!";
     *   let x = 50, y = 150;
     *
     *   fill(0);
     *   text(s, x, y);
     *
     *   // Get the descent of the current font
     *   let desc = textDescent();
     *
     *   // Draw a red line at the baseline and a blue line at the bottom of the text
     *   stroke('red');
     *   line(x, y, x + 200, y); // Baseline
     *   stroke('blue');
     *   line(x, y + desc, x + 200, y + desc); // Descent (bottom of text)
     *
     *   noStroke();
     *   fill(0);
     *   textSize(16);
     *   text("textDescent: " + desc.toFixed(2) + " pixels", x, y + desc + 20);
     * }
     *
     * @example
     * let font;
     *
     * async function setup()  {
     *   font = await loadFont('assets/inconsolata.otf');
     *
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the font.
     *   textFont(font);
     *
     *   // Different for each font.
     *   let fontScale = 0.9;
     *
     *   let baseY = 75;
     *   strokeWeight(0.5);
     *
     *   // Draw small text.
     *   textSize(24);
     *   text('dp', 0, baseY);
     *
     *   // Draw baseline and descent.
     *   let d = textDescent() * fontScale;
     *   line(0, baseY, 23, baseY);
     *   line(23, baseY, 23, baseY + d);
     *
     *   // Draw large text.
     *   textSize(48);
     *   text('dp', 45, baseY);
     *
     *   // Draw baseline and descent.
     *   d = textDescent() * fontScale;
     *   line(45, baseY, 91, baseY);
     *   line(91, baseY, 91, baseY + d);
     *
     *   describe('The letters "dp" written twice in different sizes. Each version has a horizontal baseline. A vertical line extends downward from each baseline to the bottom of the "p".');
     * }
     */

    /**
     * Sets the spacing between lines of text when
     * <a href="#/p5/text">text()</a> is called.
     *
     * Note: Spacing is measured in pixels.
     *
     * Calling `textLeading()` without an argument returns the current spacing.
     *
     * @method textLeading
     * @for p5
     * @param {Number} [leading] The new text leading to apply, in pixels
     * @returns {Number} If no arguments are provided, the current text leading
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // "\n" starts a new line of text.
     *   let lines = 'one\ntwo';
     *
     *   // Left.
     *   text(lines, 10, 25);
     *
     *   // Right.
     *   textLeading(30);
     *   text(lines, 70, 25);
     *
     *   describe('The words "one" and "two" written on separate lines twice. The words on the left have less vertical spacing than the words on the right.');
     * }
     */

    /**
     * Sets the font used by the <a href="#/p5/text">text()</a> function.
     *
     * The first parameter, `font`, sets the font. `textFont()` recognizes either
     * a <a href="#/p5.Font">p5.Font</a> object or a string with the name of a
     * system font. For example, `'Courier New'`.
     *
     * The second parameter, `size`, is optional. It sets the font size in pixels.
     * This has the same effect as calling <a href="#/p5/textSize">textSize()</a>.
     *
     * Calling `textFont()` without arguments returns the current font.
     *
     * Note: `WEBGL` mode only supports fonts loaded with
     * <a href="#/p5/loadFont">loadFont()</a>.
     *
     * @method textFont
     * @param {p5.Font|String|Object} [font] The font to apply
     * @param {Number} [size] An optional text size to apply.
     * @returns {String|p5.Font} If no arguments are provided, returns the current font
     * @for p5
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *   background(200);
     *   textFont('Courier New');
     *   textSize(24);
     *   text('hi', 35, 55);
     *
     *   describe('The text "hi" written in a black, monospace font on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *   background('black');
     *   fill('palegreen');
     *   textFont('Courier New', 10);
     *   text('You turn to the left and see a door. Do you enter?', 5, 5, 90, 90);
     *   text('>', 5, 70);
     *
     *   describe('A text prompt from a game is written in a green, monospace font on a black background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *   background(200);
     *   textFont('Verdana');
     *   let currentFont = textFont();
     *   text(currentFont, 25, 50);
     *
     *   describe('The text "Verdana" written in a black, sans-serif font on a gray background.');
     * }
     *
     * @example
     * let fontRegular;
     * let fontItalic;
     * let fontBold;
     *
     * async function setup() {
     *   createCanvas(100, 100);
     *   fontRegular = await loadFont('assets/Regular.otf');
     *   fontItalic = await loadFont('assets/Italic.ttf');
     *   fontBold = await loadFont('assets/Bold.ttf');
     *
     *   background(200);
     *   textFont(fontRegular);
     *   text('I am Normal', 10, 30);
     *   textFont(fontItalic);
     *   text('I am Italic', 10, 50);
     *   textFont(fontBold);
     *   text('I am Bold', 10, 70);
     *
     *   describe('The statements "I am Normal", "I am Italic", and "I am Bold" written in black on separate lines. The statements have normal, italic, and bold fonts, respectively.');
     * }
     */

    /**
     * Sets or gets the current text size.
     *
     * The `textSize()` function is used to specify the size of the text
     * that will be rendered on the canvas. When called with an argument, it sets the
     * text size to the specified value (which can be a number representing pixels or a
     * CSS-style string, e.g., '32px', '2em'). When called without an argument, it
     * returns the current text size in pixels.
     *
     * @method textSize
     * @for p5
     *
     * @param {Number} size - The size to set for the text.
     * @returns {Number} If no arguments are provided, the current text size in pixels.
     *
     * @example
     * function setup() {
     *   createCanvas(600, 200);
     *   background(240);
     *
     *   // Set the text size to 48 pixels
     *   textSize(48);
     *   textAlign(CENTER, CENTER);
     *   textFont("Georgia");
     *
     *   // Draw text using the current text size
     *   fill(0);
     *   text("Hello, p5.js!", width / 2, height / 2);
     *
     *   // Retrieve and display the current text size
     *   let currentSize = textSize();
     *   fill(50);
     *   textSize(16);
     *   text("Current text size: " + currentSize, width / 2, height - 20);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Top.
     *   textSize(12);
     *   text('Font Size 12', 10, 30);
     *
     *   // Middle.
     *   textSize(14);
     *   text('Font Size 14', 10, 60);
     *
     *   // Bottom.
     *   textSize(16);
     *   text('Font Size 16', 10, 90);
     *
     *   describe('The text "Font Size 12" drawn small, "Font Size 14" drawn medium, and "Font Size 16" drawn large.');
     * }
     */
    /**
     * @method textSize
     * @for p5
     * @returns {Number} The current text size in pixels.
     */

    /**
     * Sets the style for system fonts when
     * <a href="#/p5/text">text()</a> is called.
     *
     * The parameter, `style`, can be either `NORMAL`, `ITALIC`, `BOLD`, or
     * `BOLDITALIC`.
     *
     * `textStyle()` may be overridden by CSS styling. This function doesn't
     * affect fonts loaded with <a href="#/p5/loadFont">loadFont()</a>.
     *
     * @method textStyle
     * @for p5
     * @param {NORMAL|ITALIC|BOLD|BOLDITALIC} style The style to use
     * @returns {NORMAL|ITALIC|BOLD|BOLDITALIC} If no arguments are provided, the current style
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textSize(12);
     *   textAlign(CENTER);
     *
     *   // First row.
     *   textStyle(NORMAL);
     *   text('Normal', 50, 15);
     *
     *   // Second row.
     *   textStyle(ITALIC);
     *   text('Italic', 50, 40);
     *
     *   // Third row.
     *   textStyle(BOLD);
     *   text('Bold', 50, 65);
     *
     *   // Fourth row.
     *   textStyle(BOLDITALIC);
     *   text('Bold Italic', 50, 90);
     *
     *   describe('The words "Normal" displayed normally, "Italic" in italic, "Bold" in bold, and "Bold Italic" in bold italics.');
     * }
     */
    /**
     * @method textStyle
     * @for p5
     * @returns {NORMAL|BOLD|ITALIC|BOLDITALIC}
     */


    /**
     * Calculates the width of the given text string in pixels.
     *
     * The `textWidth()` function processes the provided text string to determine its tight bounding box
     * based on the current text properties such as font, textSize, and textStyle. Internally, it splits
     * the text into individual lines (if line breaks are present) and computes the bounding box for each
     * line using the renderer’s measurement functions. The final width is determined as the maximum width
     * among all these lines.
     *
     * For example, if the text contains multiple lines due to wrapping or explicit line breaks, textWidth()
     * will return the width of the longest line.
     *
     * **Note:** In p5.js 2.0+, leading and trailing spaces are ignored.
     * `textWidth("  Hello  ")` returns the same width as `textWidth("Hello")`.
     *
     * @method textWidth
     * @for p5
     * @param {String} text The text to measure
     * @returns {Number} The width of the text
     *
     * @example
     * function setup() {
     *   createCanvas(200, 200);
     *   background(220);
     *
     *   // Set text size and alignment
     *   textSize(48);
     *   textAlign(LEFT, TOP);
     *
     *   let myText = "Hello";
     *
     *   // Calculate the width of the text
     *   let tw = textWidth(myText);
     *
     *   // Draw the text on the canvas
     *   fill(0);
     *   text(myText, 50, 50);
     *
     *   // Display the text width below
     *   noStroke();
     *   fill(0);
     *   textSize(20);
     *   text("Text width: " + tw, 10, 150);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textSize(28);
     *   strokeWeight(0.5);
     *
     *   // Calculate the text width.
     *   let s = 'yoyo';
     *   let w = textWidth(s);
     *
     *   // Display the text.
     *   text(s, 22, 55);
     *
     *   // Underline the text.
     *   line(22, 55, 22 + w, 55);
     *
     *   describe('The word "yoyo" underlined.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(200, 160);
     *   background(235);
     *   noLoop();
     *
     *   textSize(18);
     *   textAlign(LEFT, TOP);
     *
     *   const x = 12, h = 24;
     *   const s1 = 'Hello';
     *   const s2 = 'Hello  ';      // 2 trailing spaces
     *   const s3 = 'Hello     ';   // many trailing spaces
     *
     *   // draw text
     *   fill(0);
     *   text(s1, x, 12);
     *   text(s2, x, 56);
     *   text(s3, x, 100);
     *
     *   // measure and draw tight boxes (all same width)
     *   noFill(); stroke(255, 0, 0);
     *   const w1 = textWidth(s1);
     *   const w2 = textWidth(s2);
     *   const w3 = textWidth(s3);
     *   rect(x, 10,  w1, h);
     *   rect(x, 54, w2, h);
     *   rect(x, 98, w3, h);
     *
     *   // small captions show the actual strings (spaces as ·)
     *   textSize(10); noStroke(); fill(30);
     *   text('"' + s1.replace(/ /g, '·') + '"  w=' + w1.toFixed(1), x, 10 + h + 2);
     *   text('"' + s2.replace(/ /g, '·') + '"  w=' + w2.toFixed(1), x, 54 + h + 2);
     *   text('"' + s3.replace(/ /g, '·') + '"  w=' + w3.toFixed(1), x, 98 + h + 2);
     *
     *   describe('Three lines: Hello with 0, 2, and many trailing spaces. Red boxes use textWidth and are identical. Captions show spaces as dots.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textSize(28);
     *   strokeWeight(0.5);
     *
     *   // Calculate the text width.
     *   // "\n" starts a new line.
     *   let s = 'yo\nyo';
     *   let w = textWidth(s);
     *
     *   // Display the text.
     *   text(s, 22, 55);
     *
     *   // Underline the text.
     *   line(22, 55, 22 + w, 55);
     *
     *   describe('The word "yo" written twice, one copy beneath the other. The words are divided by a horizontal line.');
     * }
     */

    /**
     * Sets the style for wrapping text when
     * <a href="#/p5/text">text()</a> is called.
     *
     * The parameter, `style`, can be one of the following values:
     *
     * `WORD` starts new lines of text at spaces. If a string of text doesn't
     * have spaces, it may overflow the text box and the canvas. This is the
     * default style.
     *
     * `CHAR` starts new lines as needed to stay within the text box.
     *
     * `textWrap()` only works when the maximum width is set for a text box. For
     * example, calling `text('Have a wonderful day', 0, 10, 100)` sets the
     * maximum width to 100 pixels.
     *
     * Calling `textWrap()` without an argument returns the current style.
     *
     * @method textWrap
     * @for p5
     *
     * @param {WORD|CHAR} style The wrapping style to use
     * @returns {CHAR|WORD} If no arguments are provided, the current wrapping style
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textSize(20);
     *   textWrap(WORD);
     *
     *   // Display the text.
     *   text('Have a wonderful day', 0, 10, 100);
     *
     *   describe('The text "Have a wonderful day" written across three lines.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textSize(20);
     *   textWrap(CHAR);
     *
     *   // Display the text.
     *   text('Have a wonderful day', 0, 10, 100);
     *
     *   describe('The text "Have a wonderful day" written across two lines.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textSize(20);
     *   textWrap(CHAR);
     *
     *   // Display the text.
     *   text('祝你有美好的一天', 0, 10, 100);
     *
     *   describe('The text "祝你有美好的一天" written across two lines.');
     * }
     */
    /**
     * @method textWrap
     * @for p5
     * @returns {CHAR|WORD} The current wrapping style
     */

    /**
     * Computes the tight bounding box for a block of text.
     *
     * The `textBounds()` function calculates the precise pixel boundaries that enclose
     * the rendered text based on the current text properties (such as font, textSize, textStyle, and
     * alignment). If the text spans multiple lines (due to line breaks or wrapping), the function
     * measures each line individually and then aggregates these measurements into a single bounding box.
     * The resulting object contains the x and y coordinates along with the width (w) and height (h)
     * of the text block.
     *
     * @method textBounds
     * @for p5
     *
     * @param {String} str - The text string to measure.
     * @param {Number} x - The x-coordinate where the text is drawn.
     * @param {Number} y - The y-coordinate where the text is drawn.
     * @param {Number} [width] - (Optional) The maximum width available for the text block.
     *                           When specified, the text may be wrapped to fit within this width.
     * @param {Number} [height] - (Optional) The maximum height available for the text block.
     *                            Any lines exceeding this height will be truncated.
     * @returns {Object} An object with properties `x`, `y`, `w`, and `h` that represent the tight
     *                   bounding box of the rendered text.
     *
     * @example
     * function setup() {
     *   createCanvas(300, 200);
     *   background(220);
     *
     *   // Set up text properties for clarity
     *   textSize(32);
     *   textAlign(LEFT, TOP);
     *
     *   let txt = "Hello, World!";
     *   // Compute the bounding box for the text starting at (50, 50)
     *   let bounds = textBounds(txt, 50, 50);
     *
     *   // Draw the text
     *   fill(0);
     *   text(txt, 50, 50);
     *
     *   // Draw the computed bounding box in red to visualize the measured area
     *   noFill();
     *   stroke('red');
     *   rect(bounds.x, bounds.y, bounds.w, bounds.h);
     * }
     */

    /**
     * Sets or gets the text drawing direction.
     *
     * The <code>textDirection()</code> function allows you to specify the direction in which text is
     * rendered on the canvas. When provided with a <code>direction</code> parameter (such as "ltr" for
     * left-to-right, "rtl" for right-to-left, or "inherit"), it updates the renderer's state with that
     * value and applies the new setting. When called without any arguments, it returns the current text
     * direction. This function is particularly useful for rendering text in languages with different
     * writing directions.
     *
     * @method textDirection
     * @for p5
     *
     * @param {String} direction - The text direction to set ("ltr", "rtl", or "inherit").
     * @returns {String} If no arguments are provided, the current text direction, either "ltr", "rtl", or "inherit"
     *
     * @example
     * function setup() {
     *   createCanvas(300, 300);
     *   background(240);
     *
     *   textSize(32);
     *   textFont("Georgia");
     *   textAlign(LEFT, TOP);
     *
     *   // Set text direction to right-to-left and draw Arabic text.
     *   textDirection("rtl");
     *   fill(0);
     *   text("مرحبًا!", 50, 50);
     *
     *   // Set text direction to left-to-right and draw English text.
     *   textDirection("ltr");
     *   text("Hello, p5.js!", 50, 150);
     *
     *   // Display the current text direction.
     *   textSize(16);
     *   fill(50);
     *   textAlign(LEFT, TOP);
     *   text("Current textDirection: " + textDirection(), 50, 250);
     * }
     */
    /**
     * @method textDirection
     * @for p5
     * @returns {String} The current text direction, either "ltr", "rtl", or "inherit"
     */

    /**
     * Sets or gets a single text property for the renderer.
     *
     * The `textProperty()` function allows you to set or retrieve a single text-related property,
     * such as `textAlign`, `textBaseline`, `fontStyle`, or any other property
     * that may be part of the renderer's state, its drawing context, or the canvas style.
     *
     * When called with a `prop` and a `value`, the function sets the property by checking
     * for its existence in the renderer's state, the drawing context, or the canvas style. If the property is
     * successfully modified, the function applies the updated text properties. If called with only the
     * `prop` parameter, the function returns the current value of that property.
     *
     * @method textProperty
     * @for p5
     *
     * @param {String} prop - The name of the text property to set or get.
     * @param value - The value to set for the specified text property. If omitted, the current
     *                      value of the property is returned
     * @returns If no arguments are provided, the current value of the specified text property
     *
     * @example
     * function setup() {
     *   createCanvas(300, 300);
     *   background(240);
     *
     *   // Set the text alignment to CENTER and the baseline to TOP using textProperty.
     *   textProperty("textAlign", CENTER);
     *   textProperty("textBaseline", TOP);
     *
     *   // Set additional text properties and draw the text.
     *   textSize(32);
     *   textFont("Georgia");
     *   fill(0);
     *   text("Hello, World!", width / 2, 50);
     *
     *   // Retrieve and display the current text properties.
     *   let currentAlign = textProperty("textAlign");
     *   let currentBaseline = textProperty("textBaseline");
     *
     *   textSize(16);
     *   textAlign(LEFT, TOP);
     *   fill(50);
     *   text("Current textAlign: " + currentAlign, 50, 150);
     *   text("Current textBaseline: " + currentBaseline, 50, 170);
     * }
     */
    /**
     * @method textProperty
     * @for p5
     * @param {String} prop - The name of the text property to set or get.
     * @returns The current value of the specified text property
     */

    /**
     * Gets or sets text properties in batch, similar to calling `textProperty()`
     * multiple times.
     *
     * If an object is passed in, `textProperty(key, value)` will be called for you
     * on every key/value pair in the object.
     *
     * If no arguments are passed in, an object will be returned with all the current
     * properties.
     *
     * @method textProperties
     * @for p5
     * @param {Object} properties An object whose keys are properties to set, and whose
     *                            values are what they should be set to.
     */
    /**
     * @method textProperties
     * @for p5
     * @returns {Object} An object with all the possible properties and their current values.
     */

    /**
     * Computes a generic (non-tight) bounding box for a block of text.
     *
     * The `fontBounds()` function calculates the bounding box for the text based on the
     * font's intrinsic metrics (such as `fontBoundingBoxAscent` and
     * `fontBoundingBoxDescent`). Unlike `textBounds()`, which measures the exact
     * pixel boundaries of the rendered text, `fontBounds()` provides a looser measurement
     * derived from the font’s default spacing. This measurement is useful for layout purposes where
     * a consistent approximation of the text's dimensions is desired.
     *
     * @method fontBounds
     * @for p5
     *
     * @param {String} str - The text string to measure.
     * @param {Number} x - The x-coordinate where the text is drawn.
     * @param {Number} y - The y-coordinate where the text is drawn.
     * @param {Number} [width] - (Optional) The maximum width available for the text block.
     *                           When specified, the text may be wrapped to fit within this width.
     * @param {Number} [height] - (Optional) The maximum height available for the text block.
     *                            Any lines exceeding this height will be truncated.
     * @returns {Object} An object with properties `x`, `y`, `w`, and `h` representing the loose
     *                   bounding box of the text based on the font's intrinsic metrics.
     *
     * @example
     * function setup() {
     *   createCanvas(300, 200);
     *   background(240);
     *
     *   textSize(32);
     *   textAlign(LEFT, TOP);
     *   textFont('Georgia');
     *
     *   let txt = "Hello, World!";
     *   // Compute the bounding box based on the font's intrinsic metrics
     *   let bounds = fontBounds(txt, 50, 50);
     *
     *   fill(0);
     *   text(txt, 50, 50);
     *
     *   noFill();
     *   stroke('green');
     *   rect(bounds.x, bounds.y, bounds.w, bounds.h);
     *
     *   noStroke();
     *   fill(50);
     *   textSize(15);
     *   text("Font Bounds: x=" + bounds.x.toFixed(1) + ", y=" + bounds.y.toFixed(1) +
     *        ", w=" + bounds.w.toFixed(1) + ", h=" + bounds.h.toFixed(1), 8, 100);
     * }
     */

    /**
     * Returns the loose width of a text string based on the current font.
     *
     * The `fontWidth()` function measures the width of the provided text string using
     * the font's default measurement (i.e., the width property from the text metrics returned by
     * the browser). Unlike `textWidth()`, which calculates the tight pixel boundaries
     * of the text glyphs, `fontWidth()` uses the font's intrinsic spacing, which may include
     * additional space for character spacing and kerning. This makes it useful for scenarios where
     * an approximate width is sufficient for layout and positioning.
     *
     * @method fontWidth
     * @for p5
     *
     * @param {String} theText - The text string to measure.
     * @returns {Number} The loose width of the text in pixels.
     *
     * @example
     * function setup() {
     *   createCanvas(300, 200);
     *   background(240);
     *
     *   textSize(32);
     *   textAlign(LEFT, TOP);
     *   textFont('Georgia');
     *
     *   let s = "Hello, World!";
     *   let fw = fontWidth(s);
     *
     *   fill(0);
     *   text(s, 50, 50);
     *
     *   stroke('blue');
     *   line(50, 90, 50 + fw, 90);
     *
     *   noStroke();
     *   fill(50);
     *   textSize(16);
     *   text("Font width: " + fw.toFixed(2) + " pixels", 50, 100);
     * }
     */

    /**
     * Returns the loose ascent of the text based on the font's intrinsic metrics.
     *
     * The `fontAscent()` function calculates the ascent of the text using the font's
     * intrinsic metrics (e.g., `fontBoundingBoxAscent`). This value represents the space
     * above the baseline that the font inherently occupies, and is useful for layout purposes when
     * an approximate vertical measurement is required.
     *
     * @method fontAscent
     * @for p5
     *
     * @returns {Number} The loose ascent value in pixels.
     *
     * @example
     * function setup() {
     *   createCanvas(300, 300);
     *   background(220);
     *
     *   textSize(35);
     *   textAlign(LEFT, BASELINE);
     *   textFont('Georgia');
     *
     *   let s = "Hello, p5.js!";
     *   let x = 50, y = 150;
     *
     *   fill(0);
     *   text(s, x, y);
     *
     *   // Get the font descent of the current font
     *   let fasc = fontAscent();
     *
     *   // Draw a red line at the baseline and a blue line at the ascent position
     *   stroke('red');
     *   line(x, y, x + 200, y); // Baseline
     *   stroke('blue');
     *   line(x, y - fasc, x + 200, y - fasc); // Font ascent position
     *
     *   noStroke();
     *   fill(0);
     *   textSize(16);
     *   text("fontAscent: " + fasc.toFixed(2) + " pixels", x, y + fdesc + 20);
     * }
     */

    /**
     * Returns the loose descent of the text based on the font's intrinsic metrics.
     *
     * The `fontDescent()` function calculates the descent of the text using the font's
     * intrinsic metrics (e.g., `fontBoundingBoxDescent`). This value represents the space
     * below the baseline that the font inherently occupies, and is useful for layout purposes when
     * an approximate vertical measurement is required.
     *
     * @method fontDescent
     * @for p5
     *
     * @returns {Number} The loose descent value in pixels.
     *
     * @example
     * function setup() {
     *   createCanvas(300, 300);
     *   background(220);
     *
     *   textSize(48);
     *   textAlign(LEFT, BASELINE);
     *   textFont('Georgia');
     *
     *   let s = "Hello, p5.js!";
     *   let x = 50, y = 150;
     *
     *   fill(0);
     *   text(s, x, y);
     *
     *   // Get the font descent of the current font
     *   let fdesc = fontDescent();
     *
     *   // Draw a red line at the baseline and a blue line at the descent position
     *   stroke('red');
     *   line(x, y, x + 200, y); // Baseline
     *   stroke('blue');
     *   line(x, y + fdesc, x + 200, y + fdesc); // Font descent position
     *
     *   noStroke();
     *   fill(0);
     *   textSize(16);
     *   text("fontDescent: " + fdesc.toFixed(2) + " pixels", x, y + fdesc + 20);
     * }
     */

    /**
     *
     * Sets or gets the current font weight.
     *
     * The <code>textWeight()</code> function is used to specify the weight (thickness) of the text.
     * When a numeric value is provided, it sets the font weight to that value and updates the
     * rendering properties accordingly (including the "font-variation-settings" on the canvas style).
     * When called without an argument, it returns the current font weight setting.
     *
     * @method textWeight
     * @for p5
     *
     * @param {Number} weight - The numeric weight value to set for the text.
     * @returns {Number} If no arguments are provided, the current font weight
     *
     * @example
     * function setup() {
     *   createCanvas(300, 200);
     *   background(240);
     *
     *   // Set text alignment, size, and font
     *   textAlign(LEFT, TOP);
     *   textSize(20);
     *   textFont("Georgia");
     *
     *   // Draw text with a normal weight (lighter appearance)
     *   push();
     *   textWeight(400);  // Set font weight to 400
     *   fill(0);
     *   text("Normal", 50, 50);
     *   let normalWeight = textWeight();  // Should return 400
     *   pop();
     *
     *   // Draw text with a bold weight (heavier appearance)
     *   push();
     *   textWeight(900);  // Set font weight to 900
     *   fill(0);
     *   text("Bold", 50, 100);
     *   let boldWeight = textWeight();  // Should return 900
     *   pop();
     *
     *   // Display the current font weight values on the canvas
     *   textSize(16);
     *   fill(50);
     *   text("Normal Weight: " + normalWeight, 150, 52);
     *   text("Bold Weight: " + boldWeight, 150, 100);
     * }
     *
     * @example
     * let font;
     *
     * async function setup() {
     *   createCanvas(100, 100);
     *   font = await loadFont(
     *     'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap'
     *   );
     * }
     *
     * function draw() {
     *   background(255);
     *   textFont(font);
     *   textAlign(LEFT, TOP);
     *   textSize(35);
     *   textWeight(sin(millis() * 0.002) * 200 + 400);
     *   text('p5*js', 0, 10);
     *   describe('The text p5*js pulsing its weight over time');
     * }
     */
    /**
     * @method textWeight
     * @for p5
     * @returns {Number} The current font weight
     */

    // attach each text func to p5, delegating to the renderer
    textFunctions.forEach(func => {
      fn[func] = function (...args) {
        if (!(func in Renderer.prototype)) {
          throw Error(`Renderer2D.prototype.${func} is not defined.`);
        }
        return this._renderer[func](...args);
      };
      // attach also to p5.Graphics.prototype
      p5.Graphics.prototype[func] = function (...args) {
        return this._renderer[func](...args);
      };
    });

    const RendererTextProps = {
      textAlign: { default: fn.LEFT, type: 'Context2d' },
      textBaseline: { default: fn.BASELINE, type: 'Context2d' },
      textFont: { default: { family: 'sans-serif' } },
      textLeading: { default: 15 },
      textSize: { default: 12 },
      textWrap: { default: fn.WORD },
      fontStretch: { default: fn.NORMAL, isShorthand: true },  // font-stretch: { default:  normal | ultra-condensed | extra-condensed | condensed | semi-condensed | semi-expanded | expanded | extra-expanded | ultra-expanded }
      fontWeight: { default: fn.NORMAL, isShorthand: true },   // font-stretch: { default:  normal | ultra-condensed | extra-condensed | condensed | semi-condensed | semi-expanded | expanded | extra-expanded | ultra-expanded }
      lineHeight: { default: fn.NORMAL, isShorthand: true },   // line-height: { default:  normal | number | length | percentage }
      fontVariant: { default: fn.NORMAL, isShorthand: true },  // font-variant: { default:  normal | small-caps }
      fontStyle: { default: fn.NORMAL, isShorthand: true },    // font-style: { default:  normal | italic | oblique } [was 'textStyle' in v1]
      direction: { default: 'inherit' } // direction: { default: inherit | ltr | rtl }
    };

    // note: font must be first here otherwise it may reset other properties
    const ContextTextProps = ['font', 'direction', 'fontKerning', 'fontStretch', 'fontVariantCaps', 'letterSpacing', 'textAlign', 'textBaseline', 'textRendering', 'wordSpacing'];

    // shorthand font properties that can be set with context2d.font
    const ShorthandFontProps = Object.keys(RendererTextProps)
      .filter(p => RendererTextProps[p].isShorthand);

    // allowable values for font-stretch property for context2d.font
    const FontStretchKeys = ['ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed', 'normal', 'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded'];

    let contextQueue, cachedDiv; // lazy

    ////////////////////////////// start API ///////////////////////////////

    Renderer.prototype.text = function (str, x, y, width, height) {

      let setBaseline = this.textDrawingContext().textBaseline; // store baseline

      // adjust {x,y,w,h} properties based on rectMode
      ({ x, y, width, height } = this._handleRectMode(x, y, width, height));

      // parse the lines according to width, height & linebreaks
      let lines = this._processLines(str, width, height);

      // add the adjusted positions [x,y] to each line
      lines = this._positionLines(x, y, width, height, lines);

      // render each line at the adjusted position
      lines.forEach(line => this._renderText(line.text, line.x, line.y));

      this.textDrawingContext().textBaseline = setBaseline; // restore baseline
    };

    /**
     * Computes the precise (tight) bounding box for a block of text
     * @param {String} str - the text to measure
     * @param {Number} x - the x-coordinate of the text
     * @param {Number} y - the y-coordinate of the text
     * @param {Number} width - the max width of the text block
     * @param {Number} height - the max height of the text block
     * @returns - a bounding box object for the text block: {x,y,w,h}
     * @private
     */
    Renderer.prototype.textBounds = function (str, x, y, width, height) {
      // delegate to _textBoundsSingle for measuring
      return this._computeBounds(
        textCoreConstants._TEXT_BOUNDS,
        str,
        x, y,
        width, height
      ).bounds;
    };

    /**
     * Computes a generic (non-tight) bounding box for a block of text
     * @param {String} str - the text to measure
     * @param {Number} x - the x-coordinate of the text
     * @param {Number} y - the y-coordinate of the text
     * @param {Number} width - the max width of the text block
     * @param {Number} height - the max height of the text block
     * @returns - a bounding box object for the text block: {x,y,w,h}
     * @private
     */
    Renderer.prototype.fontBounds = function (str, x, y, width, height) {
      // delegate to _fontBoundsSingle for measuring
      return this._computeBounds(
        textCoreConstants._FONT_BOUNDS,
        str,
        x, y,
        width, height
      ).bounds;
    };

    /**
     * Get the width of a text string in pixels (tight bounds)
     * @param {String} theText
     * @returns - the width of the text in pixels
     * @private
     */
    Renderer.prototype.textWidth = function (theText) {
      let lines = this._processLines(theText);
      // return the max width of the lines (using tight bounds)
      return Math.max(...lines.map(l => this._textWidthSingle(l)));
    };

    /**
     * Get the width of a text string in pixels (loose bounds)
     * @param {String} theText
     * @returns - the width of the text in pixels
     * @private
     */
    Renderer.prototype.fontWidth = function (theText) {
      // return the max width of the lines (using loose bounds)
      let lines = this._processLines(theText);
      return Math.max(...lines.map(l => this._fontWidthSingle(l)));
    };

    /**
     * @param {*} txt - optional text to measure, if provided will be
     * used to compute the ascent, otherwise the font's ascent will be used
     * @returns - the ascent of the text
     * @private
     */
    Renderer.prototype.textAscent = function (txt = '') {
      if (!txt.length) return this.fontAscent();
      return this.textDrawingContext().measureText(txt).actualBoundingBoxAscent;
    };

    /**
     * @returns - returns the ascent for the current font
     * @private
     */
    Renderer.prototype.fontAscent = function () {
      return this.textDrawingContext().measureText('_').fontBoundingBoxAscent;
    };

    /**
     * @param {*} txt - optional text to measure, if provided will
     * be used to compute the descent, otherwise the font's descent will be used
     * @returns - the descent of the text
     * @private
     */
    Renderer.prototype.textDescent = function (txt = '') {
      if (!txt.length) return this.fontDescent();
      return this.textDrawingContext().measureText(txt).actualBoundingBoxDescent;
    };

    Renderer.prototype.fontDescent = function () {
      return this.textDrawingContext().measureText('_').fontBoundingBoxDescent;
    };


    // setters/getters for text properties //////////////////////////

    Renderer.prototype.textAlign = function (h, v) {

      // the setter
      if (typeof h !== 'undefined') {
        this.states.setValue('textAlign', h);
        if (typeof v !== 'undefined') {
          if (v === fn.CENTER) {
            v = textCoreConstants._CTX_MIDDLE;
          }
          this.states.setValue('textBaseline', v);
        }
        return this._applyTextProperties();
      }
      // the getter
      return {
        horizontal: this.states.textAlign,
        vertical: this.states.textBaseline
      };
    };

    Renderer.prototype._currentTextFont = function () {
      return this.states.textFont.font || this.states.textFont.family;
    };

    /**
     * Set the font and [size] and [options] for rendering text
     * @param {p5.Font | string} font - the font to use for rendering text
     * @param {Number} size - the size of the text, can be a number or a css-style string
     * @param {Object} options - additional options for rendering text, see FontProps
     * @private
     */
    Renderer.prototype.textFont = function (font, size, options) {

      if (arguments.length === 0) {
        return this._currentTextFont();
      }

      let family = font;

      // do we have a custon loaded font ?
      if (font instanceof p5.Font) {
        family = font.face.family;
      }
      else if (font.data instanceof Uint8Array) {
        family = font.name.fontFamily;
        if (font.name?.fontSubfamily) {
          family += '-' + font.name.fontSubfamily;
        }
      }
      else if (typeof font === 'string') {
        // direct set the font-string if it contains size
        if (typeof size === 'undefined' && /[.0-9]+(%|em|p[xt])/.test(family)) {
          //console.log('direct set font-string: ', family);
          ({ family, size } = this._directSetFontString(family));
        }
      }

      if (typeof family !== 'string') throw Error('null font in textFont()');

      // handle two-arg case: textFont(font, options)
      if (arguments.length === 2 && typeof size === 'object') {
        options = size;
        size = undefined;
      }

      // update font properties in this.states
      this.states.setValue('textFont', { font, family, size });

      // convert/update the size in this.states
      if (typeof size !== 'undefined') {
        this._setTextSize(size);
      }

      // apply any options to this.states
      if (typeof options === 'object') {
        this.textProperties(options);
      }

      return this._applyTextProperties();
    };

    Renderer.prototype._directSetFontString = function (font, debug = 0) {
      if (debug) console.log('_directSetFontString"' + font + '"');

      let defaults = ShorthandFontProps.reduce((props, p) => {
        props[p] = RendererTextProps[p].default;
        return props;
      }, {});

      let el = this._cachedDiv(defaults);
      el.style.font = font;
      let style = getComputedStyle(el);
      ShorthandFontProps.forEach(prop => {
        this.states[prop] = style[prop];
        if (debug) console.log('  this.states.' + prop + '="' + style[prop] + '"');
      });

      return { family: style.fontFamily, size: style.fontSize };
    };

    Renderer.prototype.textLeading = function (leading) {
      // the setter
      if (typeof leading === 'number') {
        this.states.setValue('leadingSet', true);
        this.states.setValue('textLeading', leading);
        return this._applyTextProperties();
      }
      // the getter
      return this.states.textLeading;
    };

    Renderer.prototype.textWeight = function (weight) {
      // the setter
      if (typeof weight === 'number') {
        this.states.setValue('fontWeight', weight);
        this._applyTextProperties();

        // Safari works without weight set in the canvas style attribute, and actually
        // has buggy behavior if it is present, using the wrong weight when drawing
        // multiple times with different weights
        if (!p5.prototype._isSafari()) {
          this._setCanvasStyleProperty('font-variation-settings', `"wght" ${weight}`);
        }
        return;
      }
      // the getter
      return this.states.fontWeight;
    };

    /**
     * @param {*} size - the size of the text, can be a number or a css-style string
     * @private
     */
    Renderer.prototype.textSize = function (size) {

      // the setter
      if (typeof size !== 'undefined') {
        this._setTextSize(size);
        return this._applyTextProperties();
      }
      // the getter
      return this.states.textSize;
    };

    Renderer.prototype.textStyle = function (style) {

      // the setter
      if (typeof style !== 'undefined') {
        this.states.setValue('fontStyle', style);
        return this._applyTextProperties();
      }
      // the getter
      return this.states.fontStyle;
    };

    Renderer.prototype.textWrap = function (wrapStyle) {

      if (wrapStyle === fn.WORD || wrapStyle === fn.CHAR) {
        this.states.setValue('textWrap', wrapStyle);
        // no need to apply text properties here as not a context property
        return this._pInst;
      }
      return this.states.textWrap;
    };

    Renderer.prototype.textDirection = function (direction) {

      if (typeof direction !== 'undefined') {
        this.states.setValue('direction', direction);
        return this._applyTextProperties();
      }
      return this.states.direction;
    };

    /**
     * Sets/gets a single text property for the renderer (eg. fontStyle, fontStretch, etc.)
     * The property to be set can be a mapped or unmapped property on `this.states` or a property
     * on `this.textDrawingContext()` or on `this.canvas.style`
     * The property to get can exist in `this.states` or `this.textDrawingContext()` or `this.canvas.style`
     * @private
     */
    Renderer.prototype.textProperty = function (prop, value, opts) {

      let modified = false, debug = opts?.debug || false;

      // getter: return option from this.states or this.textDrawingContext()
      if (typeof value === 'undefined') {
        let props = this.textProperties();
        if (prop in props) return props[prop];
        throw Error('Unknown text option "' + prop + '"'); // FES?
      }

      // set the option in this.states if it exists
      if (prop in this.states && this.states[prop] !== value) {
        this.states[prop] = value;
        modified = true;
        if (debug) {
          console.log('this.states.' + prop + '="' + options[prop] + '"');
        }
      }
      // does it exist in CanvasRenderingContext2D ?
      else if (prop in this.textDrawingContext()) {
        this._setContextProperty(prop, value, debug);
        modified = true;
      }
      // does it exist in the canvas.style ?
      else if (prop in this.textCanvas().style) {
        this._setCanvasStyleProperty(prop, value, debug);
        modified = true;
      }
      else {
        console.warn('Ignoring unknown text option: "' + prop + '"\n'); // FES?
      }

      return modified ? this._applyTextProperties() : this._pInst;
    };

    /**
     * Batch set/get text properties for the renderer.
     * The properties can be either on `states` or `drawingContext`
     * @private
     */
    Renderer.prototype.textProperties = function (properties) {

      // setter
      if (typeof properties !== 'undefined') {
        Object.keys(properties).forEach(opt => {
          this.textProperty(opt, properties[opt]);
        });
        return this._pInst;
      }

      // getter: get props from drawingContext
      let context = this.textDrawingContext();
      properties = ContextTextProps.reduce((props, p) => {
        props[p] = context[p];
        return props;
      }, {});

      // add renderer props
      Object.keys(RendererTextProps).forEach(p => {
        if (RendererTextProps[p]?.type === 'Context2d') {
          properties[p] = context[p];
        }
        else { // a renderer.states property
          if (p === 'textFont') {
            // avoid circular ref. inside textFont
            let current = this._currentTextFont();
            if (typeof current === 'object' && '_pInst' in current) {
              current = Object.assign({}, current);
              delete current._pInst;
            }
            properties[p] = current;
          }
          else {
            properties[p] = this.states[p];
          }
        }
      });

      return properties;
    };

    Renderer.prototype.textMode = function () { /* no-op for processing api */ };

    /////////////////////////////// end API ////////////////////////////////

    Renderer.prototype._currentTextFont = function () {
      return this.states.textFont.font || this.states.textFont.family;
    };

    /*
      Compute the bounds for a block of text based on the specified
      measure function, either _textBoundsSingle or _fontBoundsSingle
     * @private
    */
    Renderer.prototype._computeBounds = function (
      type,
      str,
      x, y,
      width, height,
      opts
    ) {

      let context = this.textDrawingContext();
      let setBaseline = context.textBaseline;
      let { textLeading, textAlign } = this.states;

      // adjust width, height based on current rectMode
      ({ width, height } = this._rectModeAdjust(x, y, width, height));

      // parse the lines according to the width & linebreaks
      let lines = this._processLines(str, width, height);

      // get the adjusted positions [x,y] for each line
      let boxes = lines.map((line, i) => this[type].bind(this)
      (line, x, y + i * textLeading));

      if (lines.length > 1 && typeof width !== 'undefined') { // fix for #7984
        // adjust the bounding boxes for horizontal text alignment in 2d
        // the WebGL mode version does additional alignment adjustments
        boxes.forEach(bb => bb.x += p5.Renderer2D.prototype._xAlignOffset.call(this, textAlign, width));
      }

      // adjust the bounding boxes for vertical text alignment in 2d
      // the WebGL mode version does additional alignment adjustments
      p5.Renderer2D.prototype._yAlignOffset.call(this, boxes, height || 0); // fix for #7984

      // get the bounds for the text block
      let bounds = boxes[0];
      if (lines.length > 1) {

        // get the bounds for the multi-line text block
        bounds = this._aggregateBounds(boxes);

        // align the multi-line bounds
        if (!opts?.ignoreRectMode) {
          this._rectModeAlign(bounds, width || 0, height || 0);
        }
      }

      context.textBaseline = setBaseline; // restore baseline

      return { bounds, lines };
    };

    /*
      Adjust width, height of bounds based on current rectMode
     * @private
    */
    Renderer.prototype._rectModeAdjust = function (x, y, width, height) {

      if (typeof width !== 'undefined') {
        switch (this.states.rectMode) {
          case fn.CENTER:
            break;
          case fn.CORNERS:
            width -= x;
            height -= y;
            break;
          case fn.RADIUS:
            width *= 2;
            height *= 2;
            break;
        }
      }
      return { x, y, width, height };
    };

    /*
      Attempts to set a property directly on the canvas.style object
     * @private
    */
    Renderer.prototype._setCanvasStyleProperty = function (opt, val, debug) {

      let value = val.toString(); // ensure its a string

      if (debug) console.log('canvas.style.' + opt + '="' + value + '"');

      // handle variable fonts options
      if (opt === FontVariationSettings) {
        this._handleFontVariationSettings(value);
      }

      // lets try to set it on the canvas style
      this.textCanvas().style[opt] = value;

      // check if the value was set successfully
      if (this.textCanvas().style[opt] !== value) ;
    };

    /*
      Parses the fontVariationSettings string and sets the font properties, only font-weight
      working consistently across browsers at present
     * @private
    */
    Renderer.prototype._handleFontVariationSettings = function (
      value, debug = false
    ) {
      // check if the value is a string or an object
      if (typeof value === 'object') {
        value = Object.keys(value).map(k => k + ' ' + value[k]).join(', ');
      }
      let values = value.split(CommaDelimRe);
      values.forEach(v => {
        v = v.replace(/["']/g, ''); // remove quotes
        let matches = VariableAxesRe.exec(v);
        //console.log('matches: ', matches);
        if (matches && matches.length) {
          let axis = matches[0];
          // get the value to 3 digits of precision with no trailing zeros
          let val = parseFloat(parseFloat(v.replace(axis, '').trim()).toFixed(3));
          switch (axis) {
            case 'wght':
              if (debug) console.log('setting font-weight=' + val);
              // manually set the font-weight via the font string
              if (this.states.fontWeight !== val) this.textWeight(val);
              return val;
            case 'wdth':
              break;
            case 'ital':
              if (debug) console.log('setting font-style=' + (val ? 'italic' : 'normal'));
              break;
            case 'slnt':
              if (debug) console.log('setting font-style=' + (val ? 'oblique' : 'normal'));
              break;
            case 'opsz':
              if (debug) console.log('setting font-optical-size=' + val);
              break;
          }
        }
      });
    };




    /*
      For properties not directly managed by the renderer in this.states
        we check if it has a mapping to a property in this.states
      Otherwise, add the property to the context-queue for later application
    */
    Renderer.prototype._setContextProperty = function (prop, val, debug = false) {

      // check if the value is actually different, else short-circuit
      if (this.textDrawingContext()[prop] === val) {
        return this._pInst;
      }

      // otherwise, we will set the property directly on the `this.textDrawingContext()`
      // by adding [property, value] to context-queue for later application
      (contextQueue ??= []).push([prop, val]);

      if (debug) console.log('queued context2d.' + prop + '="' + val + '"');
    };

    /*
       Adjust parameters (x,y,w,h) based on current rectMode
    */
    Renderer.prototype._handleRectMode = function (x, y, width, height) {

      let rectMode = this.states.rectMode;

      if (typeof width !== 'undefined') {
        switch (rectMode) {
          case fn.RADIUS:
            width *= 2;
            x -= width / 2;
            if (typeof height !== 'undefined') {
              height *= 2;
              y -= height / 2;
            }
            break;
          case fn.CENTER:
            x -= width / 2;
            if (typeof height !== 'undefined') {
              y -= height / 2;
            }
            break;
          case fn.CORNERS:
            width -= x;
            if (typeof height !== 'undefined') {
              height -= y;
            }
            break;
        }
      }
      return { x, y, width, height };
    };

    /*
      Get the computed font-size in pixels for a given size string
      @param {String} size - the font-size string to compute
      @returns {number} - the computed font-size in pixels
     * @private
     */
    Renderer.prototype._fontSizePx = function (
      theSize,
      { family } = this.states.textFont
    ) {
      const isNumString = num => !isNaN(num) && num.trim() !== '';

      // check for a number in a string, eg '12'
      if (isNumString(theSize)) {
        return parseFloat(theSize);
      }
      let ele = this._cachedDiv({ fontSize: theSize });
      ele.style.fontSize = theSize;
      ele.style.fontFamily = family;
      let fontSizeStr = getComputedStyle(ele).fontSize;
      let fontSize = parseFloat(fontSizeStr);
      if (typeof fontSize !== 'number') {
        throw Error('textSize: invalid font-size');
      }
      return fontSize;
    };

    Renderer.prototype._cachedDiv = function (props) {
      if (typeof cachedDiv === 'undefined') {
        let ele = document.createElement('div');
        ele.ariaHidden = 'true';
        ele.style.display = 'none';
        Object.entries(props).forEach(([prop, val]) => {
          ele.style[prop] = val;
        });
        this.textCanvas().appendChild(ele);
        cachedDiv = ele;
      }
      return cachedDiv;
    };


    /*
      Aggregate the bounding boxes of multiple lines of text
      @param {Array} bboxes - the bounding boxes to aggregate
      @returns {object} - the aggregated bounding box
     * @private
    */
    Renderer.prototype._aggregateBounds = function (bboxes) {
      // loop over the bounding boxes to get the min/max x/y values
      let minX = Math.min(...bboxes.map(b => b.x));
      let minY = Math.min(...bboxes.map(b => b.y));
      let maxY = Math.max(...bboxes.map(b => b.y + b.h));
      let maxX = Math.max(...bboxes.map(b => b.x + b.w));
      return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
    };

    // Renderer.prototype._aggregateBounds = function (tx, ty, bboxes) {
    //   let x = Math.min(...bboxes.map(b => b.x));
    //   let y = Math.min(...bboxes.map(b => b.y));
    //   // the width is the max of the x-offset + the box width
    //   let w = Math.max(...bboxes.map(b => (b.x - tx) + b.w));
    //   let h = bboxes[bboxes.length - 1].y - bboxes[0].y + bboxes[bboxes.length - 1].h;


    //   return { x, y, w, h };
    // };

    /*
      Process the text string to handle line-breaks and text wrapping
      @param {String} str - the text to process
      @param {Number} width - the width to wrap the text to
      @returns {array} - the processed lines of text
     * @private
    */
    Renderer.prototype._processLines = function (str, width, height) {

      if (typeof width !== 'undefined') { // only for text with bounds
        let drawingContext = this.textDrawingContext();
        if (drawingContext.textBaseline === fn.BASELINE) {
          this.drawingContext.textBaseline = fn.TOP;
        }
      }

      let lines = this._splitOnBreaks(str.toString());
      let hasLineBreaks = lines.length > 1;
      let hasWidth = typeof width !== 'undefined';
      let exceedsWidth = hasWidth &&
        lines.some(l => this._textWidthSingle(l) > width);
      let { textLeading: leading, textWrap } = this.states;

      //if (!hasLineBreaks && !exceedsWidth) return lines; // a single-line
      if (hasLineBreaks || exceedsWidth) {
        if (hasWidth) lines = this._lineate(textWrap, lines, width);
      }

      // handle height truncation
      if (hasWidth && typeof height !== 'undefined') {

        if (typeof leading === 'undefined') {
          throw Error('leading is required if height is specified');
        }

        // truncate lines that exceed the height
        for (let i = 0; i < lines.length; i++) {
          let lh = leading * (i + 1);
          if (lh > height) {
            //console.log('TRUNCATING: ', i, '-', lines.length, '"' + lines.slice(i) + '"');
            lines = lines.slice(0, i);
            break;
          }
        }
      }

      return lines;
    };

    /*
      Get the x-offset for text given the width and textAlign property
    */
    Renderer.prototype._xAlignOffset = function (textAlign, width) {
      switch (textAlign) {
        case fn.LEFT:
          return 0;
        case fn.CENTER:
          return width / 2;
        case fn.RIGHT:
          return width;
        case textCoreConstants.START:
          return 0;
        case textCoreConstants.END:
          throw new Error('textBounds: END not yet supported for textAlign');
        default:
          return 0;
      }
    };

    /*
      Align the bounding box based on the current rectMode setting
    */
    Renderer.prototype._rectModeAlign = function (bb, width, height) {
      if (typeof width !== 'undefined') {

        switch (this.states.rectMode) {
          case fn.CENTER:
            bb.x -= (width - bb.w) / 2;
            bb.y -= (height - bb.h) / 2;
            break;
          case fn.CORNERS:
            bb.w += bb.x;
            bb.h += bb.y;
            break;
          case fn.RADIUS:
            bb.x -= (width - bb.w) / 2;
            bb.y -= (height - bb.h) / 2;
            bb.w /= 2;
            bb.h /= 2;
            break;
        }
        return bb;
      }
    };

    Renderer.prototype._rectModeAlignRevert = function (bb, width, height) {
      if (typeof width !== 'undefined') {

        switch (this.states.rectMode) {
          case fn.CENTER:
            bb.x += (width - bb.w) / 2;
            bb.y += (height - bb.h) / 2;
            break;
          case fn.CORNERS:
            bb.w -= bb.x;
            bb.h -= bb.y;
            break;
          case fn.RADIUS:
            bb.x += (width - bb.w) / 2;
            bb.y += (height - bb.h) / 2;
            bb.w *= 2;
            bb.h *= 2;
            break;
        }
        return bb;
      }
    };

    /*
      Get the (tight) width of a single line of text
    */
    Renderer.prototype._textWidthSingle = function (s) {
      let metrics = this.textDrawingContext().measureText(s);
      let abl = metrics.actualBoundingBoxLeft;
      let abr = metrics.actualBoundingBoxRight;
      return abr + abl;
    };

    /*
      Get the (loose) width of a single line of text as specified by the font
    */
    Renderer.prototype._fontWidthSingle = function (s) {
      return this.textDrawingContext().measureText(s).width;
    };

    /*
      Get the (tight) bounds of a single line of text based on its actual bounding box
    */
    Renderer.prototype._textBoundsSingle = function (s, x = 0, y = 0) {

      let metrics = this.textDrawingContext().measureText(s);
      let asc = metrics.actualBoundingBoxAscent;
      let desc = metrics.actualBoundingBoxDescent;
      let abl = metrics.actualBoundingBoxLeft;
      let abr = metrics.actualBoundingBoxRight;
      return { x: x - abl, y: y - asc, w: abr + abl, h: asc + desc };
    };

    /*
      Get the (loose) bounds of a single line of text based on its font's bounding box
    */
    Renderer.prototype._fontBoundsSingle = function (s, x = 0, y = 0) {

      let metrics = this.textDrawingContext().measureText(s);
      let asc = metrics.fontBoundingBoxAscent;
      let desc = metrics.fontBoundingBoxDescent;
      x -= this._xAlignOffset(this.states.textAlign, metrics.width);
      return { x, y: y - asc, w: metrics.width, h: asc + desc };  };

    /*
      Set the textSize property in `this.states` if it has changed
      @param {number | string} theSize - the font-size to set
      @returns {boolean} - true if the size was changed, false otherwise
     */
    Renderer.prototype._setTextSize = function (theSize) {

      if (typeof theSize === 'string') {
        // parse the size string via computed style, eg '2em'
        theSize = this._fontSizePx(theSize);
      }

      // should be a number now
      if (typeof theSize === 'number') {

        // set it in `this.states` if its been changed
        if (this.states.textSize !== theSize) {
          this.states.setValue('textSize', theSize);

          // handle leading here, if not set otherwise
          if (!this.states.leadingSet) {
            this.states.setValue('textLeading', this.states.textSize * LeadingScale);
          }
          return true; // size was changed
        }
      }
      else {
        console.warn('textSize: invalid size: ' + theSize);
      }

      return false;
    };

    /*
      Split the lines of text based on the width and the textWrap property
      @param {Array} lines - the lines of text to split
      @param {Number} maxWidth - the maximum width of the lines
      @param {Object} opts - additional options for splitting the lines
      @returns {array} - the split lines of text
     * @private
    */
    Renderer.prototype._lineate = function (
      textWrap,
      lines,
      maxWidth = Infinity,
      opts = {}
    ) {

      let splitter = opts.splitChar ?? (textWrap === fn.WORD ? ' ' : '');
      let line, testLine, testWidth, words, newLines = [];

      for (let lidx = 0; lidx < lines.length; lidx++) {
        line = '';
        words = lines[lidx].split(splitter);
        for (let widx = 0; widx < words.length; widx++) {
          testLine = `${line + words[widx]}` + splitter;
          testWidth = this._textWidthSingle(testLine);
          if (line.length > 0 && testWidth > maxWidth) {
            newLines.push(line.trim());
            line = `${words[widx]}` + splitter;
          } else {
            line = testLine;
          }
        }
        newLines.push(line.trim());
      }
      return newLines;
    };

    /*
      Split the text into lines based on line-breaks and tabs
    */
    Renderer.prototype._splitOnBreaks = function (s) {
      if (!s || s.length === 0) return [''];
      return s.replace(TabsRe, '  ').split(LinebreakRe);
    };

    /*
      Parse the font-family string to handle complex names, fallbacks, etc.
    */
    Renderer.prototype._parseFontFamily = function (familyStr) {

      let parts = familyStr.split(CommaDelimRe);
      let family = parts.map(part => {
        part = part.trim();
        if ((part.indexOf(' ') > -1 || SpecialCharRe.test(part)) && !QuotedRe.test(part)) {
          part = `"${part}"`; // quote font names with spaces
        }
        return part;
      }).join(', ');

      return family;
    };

    Renderer.prototype._applyFontString = function () {
      /*
        Create the font-string according to the CSS font-string specification:
        If font is specified as a shorthand for several font-related properties, then:
        - it must include values for: <font-size> and <font-family>
        - it may optionally include values for:
            [<font-style>, <font-variant>, <font-weight>, <font-stretch>, <line-height>]
        Format:
        - font-style, font-variant and font-weight must precede font-size
        - font-variant may only specify the values defined in CSS 2.1, that is 'normal' and 'small-caps'.
        - font-stretch may only be a single keyword value.
        - line-height must immediately follow font-size, preceded by "/", eg 16px/3.
        - font-family must be the last value specified.
      */
      let {
        textFont,
        textSize,
        lineHeight,
        fontStyle,
        fontWeight,
        fontVariant
      } = this.states;
      let drawingContext = this.textDrawingContext();

      let family = this._parseFontFamily(textFont.family);
      let style = fontStyle !== fn.NORMAL ? `${fontStyle} ` : '';
      let weight = fontWeight !== fn.NORMAL ? `${fontWeight} ` : '';
      let variant = fontVariant !== fn.NORMAL ? `${fontVariant} ` : '';
      let fsize = `${textSize}px` + (lineHeight !== fn.NORMAL ? `/${lineHeight} ` : ' ');
      let fontString = `${style}${variant}${weight}${fsize}${family}`.trim();
      //console.log('fontString="' + fontString + '"');

      // set the font string on the context
      drawingContext.font = fontString;

      // verify that it was set successfully
      if (drawingContext.font !== fontString) {
        let expected = fontString;
        let actual = drawingContext.font;
        if (expected !== actual) {
          //console.warn(`Unable to set font property on context2d. It may not be supported.`);
          //console.log('Expected "' + expected + '" but got: "' + actual + '"'); // TMP
          return false;
        }
      }
      return true;
    };

    /*
      Apply the text properties in `this.states` to the `this.textDrawingContext()`
      Then apply any properties in the context-queue
     */
    Renderer.prototype._applyTextProperties = function (debug = false) {

      this._applyFontString();

      // set these after the font so they're not overridden
      let context = this.textDrawingContext();
      context.direction = this.states.direction;
      context.textAlign = this.states.textAlign;
      context.textBaseline = this.states.textBaseline;

      // set manually as (still) not fully supported as part of font-string
      let stretch = this.states.fontStretch;
      if (FontStretchKeys.includes(stretch) && context.fontStretch !== stretch) {
        context.fontStretch = stretch;
      }

      // apply each property in queue after the font so they're not overridden
      while (contextQueue?.length) {

        let [prop, val] = contextQueue.shift();
        if (debug) console.log('apply context property "' + prop + '" = "' + val + '"');
        context[prop] = val;

        // check if the value was set successfully
        if (context[prop] !== val) {
          console.warn(`Unable to set '${prop}' property on context2d. It may not be supported.`); // FES?
          console.log('Expected "' + val + '" but got: "' + context[prop] + '"');
        }
      }

      return this._pInst;
    };
  }

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

  /*
   * Creates p5.strands filter shaders for cross-platform compatibility.
   *
   * NOTE: These work a little differently than p5.js web editor shaders work!
   * Firstly, it uses instance mode, so we have to explicitly pass in context
   * variables in an argument to your callback and as a second argument to `modify`.
   * Secondly, always manually specify uniform names, as variable names will change
   * in minified builds.
   */
  function makeFilterShader(renderer, operation, p5) {
    switch (operation) {
      case GRAY:
        return renderer.baseFilterShader().modify(({ p5 }) => {
          p5.getColor((inputs, canvasContent) => {
            const tex = p5.getTexture(canvasContent, inputs.texCoord);
            // weighted grayscale with luminance values
            const gray = p5.dot(tex.rgb, p5.vec3(0.2126, 0.7152, 0.0722));
            return p5.vec4(gray, gray, gray, tex.a);
          });
        }, { p5 });

      case INVERT:
        return renderer.baseFilterShader().modify(({ p5 }) => {
          p5.getColor((inputs, canvasContent) => {
            const color = p5.getTexture(canvasContent, inputs.texCoord);
            const invertedColor = p5.vec3(1.0) - color.rgb;
            return p5.vec4(invertedColor, color.a);
          });
        }, { p5 });

      case THRESHOLD:
        return renderer.baseFilterShader().modify(({ p5 }) => {
          const filterParameter = p5.uniformFloat('filterParameter');
          p5.getColor((inputs, canvasContent) => {
            const color = p5.getTexture(canvasContent, inputs.texCoord);
            // weighted grayscale with luminance values
            const gray = p5.dot(color.rgb, p5.vec3(0.2126, 0.7152, 0.0722));
            const threshold = p5.floor(filterParameter * 255.0) / 255;
            const blackOrWhite = p5.step(threshold, gray);
            return p5.vec4(p5.vec3(blackOrWhite), color.a);
          });
        }, { p5 });

      case POSTERIZE:
        return renderer.baseFilterShader().modify(({ p5 }) => {
          const filterParameter = p5.uniformFloat('filterParameter');
          const quantize = (color, n) => {
            // restrict values to N options/bins
            // and floor each channel to nearest value
            //
            // eg. when N = 5, values = 0.0, 0.25, 0.50, 0.75, 1.0
            // then quantize (0.1, 0.7, 0.9) -> (0.0, 0.5, 1.0)

            color = color * n;
            color = p5.floor(color);
            color = color / (n - 1.0);
            return color;
          };
          p5.getColor((inputs, canvasContent) => {
            const color = p5.getTexture(canvasContent, inputs.texCoord);
            const restrictedColor = quantize(color.rgb, filterParameter);
            return p5.vec4(restrictedColor, color.a);
          });
        }, { p5 });

      case BLUR:
        return renderer.baseFilterShader().modify(({ p5 }) => {
          const radius = p5.uniformFloat('radius');
          const direction = p5.uniformVec2('direction');

          // This isn't a real Gaussian weight, it's a quadratic weight
          const quadWeight = (x, e) => {
            return p5.pow(e - p5.abs(x), 2.0);
          };

          const random = (p) => {
            let p3 = p5.fract(p.xyx * .1031);
            p3 += p5.dot(p3, p3.yzx + 33.33);
            return p5.fract((p3.x + p3.y) * p3.z);
          };

          p5.getColor((inputs, canvasContent) => {
            const uv = inputs.texCoord;

            // A reasonable maximum number of samples
            const maxSamples = 64.0;

            let numSamples = p5.floor(radius * 7.0);
            if (p5.mod(numSamples, 2) == 0.0) {
              numSamples++;
            }

            let avg = p5.vec4(0.0);
            let total = 0.0;

            // Calculate the spacing to avoid skewing if numSamples > maxSamples
            let spacing = 1.0;
            if (numSamples > maxSamples) {
              spacing = numSamples / maxSamples;
              numSamples = maxSamples;
            }

            const randomOffset = (spacing - 1.0) * p5.mix(-0.5, 0.5, random(uv * inputs.canvasSize));
            for (let i = 0; i < numSamples; i++) {
              const sample = i * spacing - (numSamples - 1.0) * 0.5 * spacing + randomOffset;
              const sampleCoord = uv + p5.vec2(sample, sample) / inputs.canvasSize * direction;
              const weight = quadWeight(sample, (numSamples - 1.0) * 0.5 * spacing);

              const texSample = p5.getTexture(canvasContent, sampleCoord);
              avg += weight * texSample * p5.vec4(
                texSample.a, texSample.a, texSample.a, 1
              );
              total += weight;
            }

            const blended = avg / total;
            return p5.vec4(
              blended.r / blended.a,
              blended.g / blended.a,
              blended.b / blended.a,
              blended.a
            );
          });
        }, { p5 });

      case ERODE:
        return renderer.baseFilterShader().modify(({ p5 }) => {
          const luma = (color) => {
            return p5.dot(color.rgb, p5.vec3(0.2126, 0.7152, 0.0722));
          };

          p5.getColor((inputs, canvasContent) => {
            const uv = inputs.texCoord;
            let minColor = p5.getTexture(canvasContent, uv);
            let minLuma = luma(minColor);

            for (let x = -1; x <= 1; x++) {
              for (let y = -1; y <= 1; y++) {
                if (x != 0 || y != 0) {
                  const offset = p5.vec2(x, y) * inputs.texelSize;
                  const neighborColor = p5.getTexture(canvasContent, uv + offset);
                  const neighborLuma = luma(neighborColor);

                  if (neighborLuma < minLuma) {
                    minLuma = neighborLuma;
                    minColor = neighborColor;
                  }
                }
              }
            }

            return minColor;
          });
        }, { p5 });

      case DILATE:
        return renderer.baseFilterShader().modify(({ p5 }) => {
          const luma = (color) => {
            return p5.dot(color.rgb, p5.vec3(0.2126, 0.7152, 0.0722));
          };

          p5.getColor((inputs, canvasContent) => {
            const uv = inputs.texCoord;
            let maxColor = p5.getTexture(canvasContent, uv);
            let maxLuma = luma(maxColor);

            for (let x = -1; x <= 1; x++) {
              for (let y = -1; y <= 1; y++) {
                if (x != 0 || y != 0) {
                  const offset = p5.vec2(x, y) * inputs.texelSize;
                  const neighborColor = p5.getTexture(canvasContent, uv + offset);
                  const neighborLuma = luma(neighborColor);

                  if (neighborLuma > maxLuma) {
                    maxLuma = neighborLuma;
                    maxColor = neighborColor;
                  }
                }
              }
            }

            return maxColor;
          });
        }, { p5 });

      case OPAQUE:
        return renderer.baseFilterShader().modify(({ p5 }) => {
          p5.getColor((inputs, canvasContent) => {
            const color = p5.getTexture(canvasContent, inputs.texCoord);
            return p5.vec4(color.rgb, 1.0);
          });
        }, { p5 });

      default:
        throw new Error(`Unknown filter: ${operation}`);
    }
  }

  function getStrokeDefs(shaderConstant) {
    const STROKE_CAP_ENUM = {};
    const STROKE_JOIN_ENUM = {};
    let lineDefs = "";
    const defineStrokeCapEnum = function (key, val) {
      lineDefs += shaderConstant(`STROKE_CAP_${key}`, `${val}`, 'u32');
      STROKE_CAP_ENUM[constants[key]] = val;
    };
    const defineStrokeJoinEnum = function (key, val) {
      lineDefs += shaderConstant(`STROKE_JOIN_${key}`, `${val}`, 'u32');
      STROKE_JOIN_ENUM[constants[key]] = val;
    };

    // Define constants in line shaders for each type of cap/join, and also record
    // the values in JS objects
    defineStrokeCapEnum("ROUND", 0);
    defineStrokeCapEnum("PROJECT", 1);
    defineStrokeCapEnum("SQUARE", 2);
    defineStrokeJoinEnum("ROUND", 0);
    defineStrokeJoinEnum("MITER", 1);
    defineStrokeJoinEnum("BEVEL", 2);

    return { STROKE_CAP_ENUM, STROKE_JOIN_ENUM, lineDefs };
  }

  const { STROKE_CAP_ENUM, STROKE_JOIN_ENUM } = getStrokeDefs(()=>"");

  class Renderer3D extends Renderer {
    constructor(pInst, w, h, isMainCanvas, elt) {
      super(pInst, w, h, isMainCanvas);

      // Create new canvas
      this.canvas = this.elt = elt || document.createElement("canvas");
      this.contextReady = this.setupContext();

      if (this._isMainCanvas) {
        // for pixel method sharing with pimage
        this._pInst._curElement = this;
        this._pInst.canvas = this.canvas;
      } else {
        // hide if offscreen buffer by default
        this.canvas.style.display = "none";
      }
      this.elt.id = "defaultCanvas0";
      this.elt.classList.add("p5Canvas");

      // Set and return p5.Element
      this.wrappedElt = new Element(this.elt, this._pInst);

      // Extend renderer with methods of p5.Element with getters
      for (const p of Object.getOwnPropertyNames(Element.prototype)) {
        if (p !== 'constructor' && p[0] !== '_') {
          Object.defineProperty(this, p, {
            get() {
              return this.wrappedElt[p];
            }
          });
        }
      }

      const dimensions = this._adjustDimensions(w, h);
      w = dimensions.adjustedWidth;
      h = dimensions.adjustedHeight;

      this.width = w;
      this.height = h;

      // Set canvas size
      this.elt.width = w * this._pixelDensity;
      this.elt.height = h * this._pixelDensity;
      this.elt.style.width = `${w}px`;
      this.elt.style.height = `${h}px`;
      this._updateViewport();

      // Attach canvas element to DOM
      if (this._pInst._userNode) {
        // user input node case
        this._pInst._userNode.appendChild(this.elt);
      } else {
        //create main element
        if (document.getElementsByTagName("main").length === 0) {
          let m = document.createElement("main");
          document.body.appendChild(m);
        }
        //append canvas to main
        document.getElementsByTagName("main")[0].appendChild(this.elt);
      }

      this.isP3D = true; //lets us know we're in 3d mode

      // When constructing a new Geometry, this will represent the builder
      this.geometryBuilder = undefined;

      // Push/pop state
      this.states.uModelMatrix = new Matrix(4);
      this.states.uViewMatrix = new Matrix(4);
      this.states.uPMatrix = new Matrix(4);

      this.mainCamera = new Camera(this);
      if (!this.states.curCamera) {
        this.states.curCamera = this.mainCamera;
      }
      this.states.uPMatrix.set(this.states.curCamera.projMatrix);
      this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix);

      this.states.enableLighting = false;
      this.states.ambientLightColors = [];
      this.states.specularColors = [1, 1, 1];
      this.states.directionalLightDirections = [];
      this.states.directionalLightDiffuseColors = [];
      this.states.directionalLightSpecularColors = [];
      this.states.pointLightPositions = [];
      this.states.pointLightDiffuseColors = [];
      this.states.pointLightSpecularColors = [];
      this.states.spotLightPositions = [];
      this.states.spotLightDirections = [];
      this.states.spotLightDiffuseColors = [];
      this.states.spotLightSpecularColors = [];
      this.states.spotLightAngle = [];
      this.states.spotLightConc = [];
      this.states.activeImageLight = null;

      this.states.curFillColor = [1, 1, 1, 1];
      this.states.curAmbientColor = [1, 1, 1, 1];
      this.states.curSpecularColor = [0, 0, 0, 0];
      this.states.curEmissiveColor = [0, 0, 0, 0];
      this.states.curStrokeColor = [0, 0, 0, 1];

      this.states.curBlendMode = BLEND;

      this.states._hasSetAmbient = false;
      this.states._useSpecularMaterial = false;
      this.states._useEmissiveMaterial = false;
      this.states._useNormalMaterial = false;
      this.states._useShininess = 1;
      this.states._useMetalness = 0;

      this.states.tint = [255, 255, 255, 255];

      this.states.constantAttenuation = 1;
      this.states.linearAttenuation = 0;
      this.states.quadraticAttenuation = 0;

      this.states._currentNormal = new Vector(0, 0, 1);

      this.states.drawMode = FILL;

      this.states._tex = null;
      this.states.textureMode = IMAGE;
      this.states.textureWrapX = CLAMP;
      this.states.textureWrapY = CLAMP;

      // erasing
      this._isErasing = false;

      // simple lines
      this._simpleLines = false;

      // clipping
      this._clipDepths = [];
      this._isClipApplied = false;
      this._stencilTestOn = false;

      this.mixedAmbientLight = [];
      this.mixedSpecularColor = [];

      // p5.framebuffer for this are calculated in getDiffusedTexture function
      this.diffusedTextures = new Map();
      // p5.framebuffer for this are calculated in getSpecularTexture function
      this.specularTextures = new Map();

      this.preEraseBlend = undefined;
      this._cachedFillStyle = [1, 1, 1, 1];
      this._cachedStrokeStyle = [0, 0, 0, 1];
      this._isBlending = false;

      this._useLineColor = false;
      this._useVertexColor = false;

      this.registerEnabled = new Set();

      // Camera
      this.mainCamera._computeCameraDefaultSettings();
      this.mainCamera._setDefaultCamera();

      // FilterCamera
      this.filterCamera = new Camera(this);
      this.filterCamera._computeCameraDefaultSettings();
      this.filterCamera._setDefaultCamera();
      // Information about the previous frame's touch object
      // for executing orbitControl()
      this.prevTouches = [];
      // Velocity variable for use with orbitControl()
      this.zoomVelocity = 0;
      this.rotateVelocity = new Vector(0, 0);
      this.moveVelocity = new Vector(0, 0);
      // Flags for recording the state of zooming, rotation and moving
      this.executeZoom = false;
      this.executeRotateAndMove = false;

      this._drawingFilter = false;
      this._drawingImage = false;

      this.specularShader = undefined;
      this.sphereMapping = undefined;
      this.diffusedShader = undefined;
      this._baseFilterShader = undefined;
      this._defaultLightShader = undefined;
      this._defaultImmediateModeShader = undefined;
      this._defaultNormalShader = undefined;
      this._defaultColorShader = undefined;

      this.states.userFillShader = undefined;
      this.states.userStrokeShader = undefined;
      this.states.userImageShader = undefined;

      this.states.curveDetail = 1 / 4;

      // Used by beginShape/endShape functions to construct a p5.Geometry
      this.shapeBuilder = new ShapeBuilder(this);

      this.geometryBufferCache = new GeometryBufferCache(this);

      this.curStrokeCap = ROUND;
      this.curStrokeJoin = ROUND;

      // map of texture sources to textures created in this gl context via this.getTexture(src)
      this.textures = new Map();

      // set of framebuffers in use
      this.framebuffers = new Set();
      // stack of active framebuffers
      this.activeFramebuffers = [];

      // for post processing step
      this.states.filterShader = undefined;
      this.filterLayer = undefined;
      this.filterLayerTemp = undefined;
      this.defaultFilterShaders = {};

      this.fontInfos = {};

      this._curShader = undefined;
      this.drawShapeCount = 1;

      this.scratchMat3 = new Matrix(3);

      // Whether or not to remove degenerate faces from geometry. This is usually
      // set to false for performance.
      this._validateFaces = false;

      this.buffers = {
        fill: [
          new RenderBuffer(
            3,
            "vertices",
            "vertexBuffer",
            "aPosition",
            this,
            this._vToNArray
          ),
          new RenderBuffer(
            3,
            "vertexNormals",
            "normalBuffer",
            "aNormal",
            this,
            this._vToNArray
          ),
          new RenderBuffer(
            4,
            "vertexColors",
            "colorBuffer",
            "aVertexColor",
            this
          ).default((geometry) => geometry.vertices.flatMap(() => [-1, -1, -1, -1])),
          new RenderBuffer(
            3,
            "vertexAmbients",
            "ambientBuffer",
            "aAmbientColor",
            this
          ),
          new RenderBuffer(2, "uvs", "uvBuffer", "aTexCoord", this, (arr) =>
            arr.flat()
          ),
        ],
        stroke: [
          new RenderBuffer(
            4,
            "lineVertexColors",
            "lineColorBuffer",
            "aVertexColor",
            this
          ).default((geometry) => geometry.lineVertices.flatMap(() => [-1, -1, -1, -1])),
          new RenderBuffer(
            3,
            "lineVertices",
            "lineVerticesBuffer",
            "aPosition",
            this
          ),
          new RenderBuffer(
            3,
            "lineTangentsIn",
            "lineTangentsInBuffer",
            "aTangentIn",
            this
          ),
          new RenderBuffer(
            3,
            "lineTangentsOut",
            "lineTangentsOutBuffer",
            "aTangentOut",
            this
          ),
          new RenderBuffer(1, "lineSides", "lineSidesBuffer", "aSide", this),
        ],
        text: [
          new RenderBuffer(
            3,
            "vertices",
            "vertexBuffer",
            "aPosition",
            this,
            this._vToNArray
          ),
          new RenderBuffer(2, "uvs", "uvBuffer", "aTexCoord", this, (arr) =>
            arr.flat()
          ),
        ],
        user: [],
      };
    }

    //This is helper function to reset the context anytime the attributes
    //are changed with setAttributes()

    async _resetContext(options, callback, ctor = Renderer3D) {
      const w = this.width;
      const h = this.height;
      const defaultId = this.canvas.id;
      const isPGraphics = this._pInst instanceof Graphics;

      // Preserve existing position and styles before recreation
      const prevStyle = {
        position: this.canvas.style.position,
        top: this.canvas.style.top,
        left: this.canvas.style.left,
      };

      if (isPGraphics) {
        // Handle PGraphics: remove and recreate the canvas
        const pg = this._pInst;
        pg.canvas.parentNode.removeChild(pg.canvas);
        pg.canvas = document.createElement("canvas");
        const node = pg._pInst._userNode || document.body;
        node.appendChild(pg.canvas);
        Element.call(pg, pg.canvas, pg._pInst);
        // Restore previous width and height
        pg.width = w;
        pg.height = h;
      } else {
        // Handle main canvas: remove and recreate it
        let c = this.canvas;
        if (c) {
          c.parentNode.removeChild(c);
        }
        c = document.createElement("canvas");
        c.id = defaultId;
        // Attach the new canvas to the correct parent node
        if (this._pInst._userNode) {
          this._pInst._userNode.appendChild(c);
        } else {
          document.body.appendChild(c);
        }
        this._pInst.canvas = c;
        this.canvas = c;

        // Restore the saved position
        this.canvas.style.position = prevStyle.position;
        this.canvas.style.top = prevStyle.top;
        this.canvas.style.left = prevStyle.left;
      }

      const renderer = new ctor(
        this._pInst,
        w,
        h,
        !isPGraphics,
        this._pInst.canvas
      );
      this._pInst._renderer = renderer;

      renderer._applyDefaults();

      if (renderer.contextReady) {
        await renderer.contextReady;
      }

      if (typeof callback === "function") {
        //setTimeout with 0 forces the task to the back of the queue, this ensures that
        //we finish switching out the renderer
        setTimeout(() => {
          callback.apply(window._renderer, options);
        }, 0);
      }
    }

    remove() {
      this.wrappedElt.remove();
      this.wrappedElt = null;
      this.canvas = null;
      this.elt = null;
    }

    //////////////////////////////////////////////
    // Geometry Building
    //////////////////////////////////////////////

    /**
     * Starts creating a new p5.Geometry. Subsequent shapes drawn will be added
     * to the geometry and then returned when
     * <a href="#/p5/endGeometry">endGeometry()</a> is called. One can also use
     * <a href="#/p5/buildGeometry">buildGeometry()</a> to pass a function that
     * draws shapes.
     *
     * If you need to draw complex shapes every frame which don't change over time,
     * combining them upfront with `beginGeometry()` and `endGeometry()` and then
     * drawing that will run faster than repeatedly drawing the individual pieces.
     * @private
     */
    beginGeometry() {
      if (this.geometryBuilder) {
        throw new Error(
          "It looks like `beginGeometry()` is being called while another p5.Geometry is already being build."
        );
      }
      this.geometryBuilder = new GeometryBuilder(this);
      this.geometryBuilder.prevFillColor = this.states.fillColor;
      this.fill(new Color([-1, -1, -1, -1]));
    }

    /**
     * Finishes creating a new <a href="#/p5.Geometry">p5.Geometry</a> that was
     * started using <a href="#/p5/beginGeometry">beginGeometry()</a>. One can also
     * use <a href="#/p5/buildGeometry">buildGeometry()</a> to pass a function that
     * draws shapes.
     * @private
     *
     * @returns {p5.Geometry} The model that was built.
     */
    endGeometry() {
      if (!this.geometryBuilder) {
        throw new Error(
          "Make sure you call beginGeometry() before endGeometry()!"
        );
      }
      const geometry = this.geometryBuilder.finish();
      if (this.geometryBuilder.prevFillColor) {
        this.fill(this.geometryBuilder.prevFillColor);
      } else {
        this.noFill();
      }
      this.geometryBuilder = undefined;
      return geometry;
    }

    /**
     * Creates a new <a href="#/p5.Geometry">p5.Geometry</a> that contains all
     * the shapes drawn in a provided callback function. The returned combined shape
     * can then be drawn all at once using <a href="#/p5/model">model()</a>.
     *
     * If you need to draw complex shapes every frame which don't change over time,
     * combining them with `buildGeometry()` once and then drawing that will run
     * faster than repeatedly drawing the individual pieces.
     *
     * One can also draw shapes directly between
     * <a href="#/p5/beginGeometry">beginGeometry()</a> and
     * <a href="#/p5/endGeometry">endGeometry()</a> instead of using a callback
     * function.
     * @param {Function} callback A function that draws shapes.
     * @returns {p5.Geometry} The model that was built from the callback function.
     */
    buildGeometry(callback) {
      this.beginGeometry();
      callback();
      return this.endGeometry();
    }

    //////////////////////////////////////////////
    // Shape drawing
    //////////////////////////////////////////////

    beginShape(...args) {
      super.beginShape(...args);
      // TODO remove when shape refactor is complete
      // this.shapeBuilder.beginShape(...args);
    }

    curveDetail(d) {
      if (d === undefined) {
        return this.states.curveDetail;
      } else {
        this.states.setValue("curveDetail", d);
      }
    }

    drawShape(shape) {
      const visitor = new PrimitiveToVerticesConverter({
        curveDetail: this.states.curveDetail,
      });
      shape.accept(visitor);
      this.shapeBuilder.constructFromContours(shape, visitor.contours);

      if (this.geometryBuilder) {
        this.geometryBuilder.addImmediate(
          this.shapeBuilder.geometry,
          this.shapeBuilder.shapeMode,
          { validateFaces: this._validateFaces }
        );
      } else if (this.states.fillColor || this.states.strokeColor) {
        this._drawGeometry(this.shapeBuilder.geometry, {
          mode: this.shapeBuilder.shapeMode,
          count: this.drawShapeCount
        });
      }
      this.drawShapeCount = 1;
    }

    endShape(mode, count) {
      this.drawShapeCount = count;
      super.endShape(mode, count);
    }

    vertexProperty(...args) {
      this.currentShape.vertexProperty(...args);
    }

    normal(xorv, y, z) {
      if (xorv instanceof Vector) {
        this.states.setValue("_currentNormal", xorv);
      } else {
        this.states.setValue("_currentNormal", new Vector(xorv, y, z));
      }
      this.updateShapeVertexProperties();
    }

    model(model, count = 1) {
      if (model.vertices.length > 0) {
        if (this.geometryBuilder) {
          this.geometryBuilder.addRetained(model);
        } else {
          if (!this.geometryInHash(model.gid)) {
            model._edgesToVertices();
            this._getOrMakeCachedBuffers(model);
          }

          this._drawGeometry(model, { count });
        }
      }
    }

    _getOrMakeCachedBuffers(geometry) {
      return this.geometryBufferCache.ensureCached(geometry);
    }

    //////////////////////////////////////////////
    // Rendering
    //////////////////////////////////////////////

    _drawGeometry(geometry, { mode = TRIANGLES, count = 1 } = {}) {
      for (const propName in geometry.userVertexProperties) {
        const prop = geometry.userVertexProperties[propName];
        this.buffers.user.push(
          new RenderBuffer(
            prop.getDataSize(),
            prop.getSrcName(),
            prop.getDstName(),
            prop.getName(),
            this
          )
        );
      }

      if (
        this.states.fillColor &&
        geometry.vertices.length >= 3 &&
        ![LINES, POINTS].includes(mode)
      ) {
        this._drawFills(geometry, { mode, count });
      }

      if (this.states.strokeColor && geometry.lineVertices.length >= 1) {
        this._drawStrokes(geometry, { count });
      }

      this.buffers.user = [];
    }

    _drawFills(geometry, { count, mode } = {}) {
      this._useVertexColor = geometry.vertexColors.length > 0 &&
        !geometry.vertexColors.isDefault;

      const shader =
        !this._drawingFilter && this.states.userFillShader
          ? this.states.userFillShader
          : this._getFillShader();
      shader.bindShader('fill');
      this._setGlobalUniforms(shader);
      this._setFillUniforms(shader);
      shader.bindTextures();

      for (const buff of this.buffers.fill) {
        buff._prepareBuffer(geometry, shader);
      }
      this._prepareUserAttributes(geometry, shader);
      this._disableRemainingAttributes(shader);

      this._applyColorBlend(
        this.states.curFillColor,
        geometry.hasFillTransparency()
      );

      this._drawBuffers(geometry, { mode, count });

      shader.unbindShader();
    }

    _drawStrokes(geometry, { count } = {}) {

      this._useLineColor = geometry.vertexStrokeColors.length > 0;

      const shader = this._getStrokeShader();
      shader.bindShader('stroke');
      this._setGlobalUniforms(shader);
      this._setStrokeUniforms(shader);
      shader.bindTextures();

      for (const buff of this.buffers.stroke) {
        buff._prepareBuffer(geometry, shader);
      }
      this._prepareUserAttributes(geometry, shader);
      this._disableRemainingAttributes(shader);

      this._applyColorBlend(
        this.states.curStrokeColor,
        geometry.hasStrokeTransparency()
      );

      this._drawBuffers(geometry, {count});

      shader.unbindShader();
    }

    _prepareUserAttributes(geometry, shader) {
      for (const buff of this.buffers.user) {
        if (!this._pInst.constructor.disableFriendleErrors) {
          // Check for the right data size
          const prop = geometry.userVertexProperties[buff.attr];
          if (prop) {
            const adjustedLength = prop.getSrcArray().length / prop.getDataSize();
            if (adjustedLength > geometry.vertices.length) {
              this._pInst.constructor._friendlyError(
                `One of the geometries has a custom vertex property '${prop.getName()}' with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`,
                "vertexProperty()"
              );
            } else if (adjustedLength < geometry.vertices.length) {
              this._pInst.constructor._friendlyError(
                `One of the geometries has a custom vertex property '${prop.getName()}' with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`,
                "vertexProperty()"
              );
            }
          }
        }
        buff._prepareBuffer(geometry, shader);
      }
    }

    _drawGeometryScaled(model, scaleX, scaleY, scaleZ) {
      let originalModelMatrix = this.states.uModelMatrix;
      this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone());
      try {
        this.states.uModelMatrix.scale(scaleX, scaleY, scaleZ);

        if (this.geometryBuilder) {
          this.geometryBuilder.addRetained(model);
        } else {
          this._drawGeometry(model);
        }
      } finally {
        this.states.setValue("uModelMatrix", originalModelMatrix);
      }
    }

    _update() {
      // reset model view and apply initial camera transform
      // (containing only look at info; no projection).
      this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone());
      this.states.uModelMatrix.reset();
      this.states.setValue("uViewMatrix", this.states.uViewMatrix.clone());
      this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix);

      // reset light data for new frame.

      this.states.setValue("ambientLightColors", []);
      this.states.setValue("specularColors", [1, 1, 1]);

      this.states.setValue("directionalLightDirections", []);
      this.states.setValue("directionalLightDiffuseColors", []);
      this.states.setValue("directionalLightSpecularColors", []);

      this.states.setValue("pointLightPositions", []);
      this.states.setValue("pointLightDiffuseColors", []);
      this.states.setValue("pointLightSpecularColors", []);

      this.states.setValue("spotLightPositions", []);
      this.states.setValue("spotLightDirections", []);
      this.states.setValue("spotLightDiffuseColors", []);
      this.states.setValue("spotLightSpecularColors", []);
      this.states.setValue("spotLightAngle", []);
      this.states.setValue("spotLightConc", []);

      this.states.setValue("enableLighting", false);

      //reset tint value for new frame
      this.states.setValue("tint", [255, 255, 255, 255]);

      //Clear depth every frame
      this._resetBuffersBeforeDraw();
    }

    background(...args) {
      const a0 = args[0];

      const isImageLike =
        a0 != null &&
        typeof a0 === 'object' &&
        typeof a0.width === 'number' &&
        typeof a0.height === 'number' &&
        (a0.canvas != null || a0.elt != null);

      // WEBGL / 3D: support background(image-like)
      if (isImageLike) {
        this._pInst.clear();
        this._pInst.push();
        this._pInst.resetMatrix();
        this._pInst.imageMode(CENTER);
        this._pInst.image(a0, 0, 0, this._pInst.width, this._pInst.height);
        this._pInst.pop();
        return;
      }

      // Default: background(color)
      const _col = this._pInst.color(...args);
      this.clear(..._col._getRGBA());
    }

    //////////////////////////////////////////////
    // Positioning
    //////////////////////////////////////////////

    get uModelMatrix() {
      return this.states.uModelMatrix;
    }

    get uViewMatrix() {
      return this.states.uViewMatrix;
    }

    get uPMatrix() {
      return this.states.uPMatrix;
    }

    get uMVMatrix() {
      const m = this.uModelMatrix.copy();
      m.mult(this.uViewMatrix);
      return m;
    }

    /**
     * Get a matrix from world-space to screen-space
     */
    getWorldToScreenMatrix() {
      const modelMatrix = this.states.uModelMatrix;
      const viewMatrix = this.states.uViewMatrix;
      const projectionMatrix = this.states.uPMatrix;
      const projectedToScreenMatrix = new Matrix(4);
      projectedToScreenMatrix.scale(this.width, this.height, 1);
      projectedToScreenMatrix.translate([0.5, 0.5, 0.5]);
      projectedToScreenMatrix.scale(0.5, -0.5, 0.5);

      const modelViewMatrix = modelMatrix.copy().mult(viewMatrix);
      const modelViewProjectionMatrix = modelViewMatrix.mult(projectionMatrix);
      const worldToScreenMatrix = modelViewProjectionMatrix
        .mult(projectedToScreenMatrix);
      return worldToScreenMatrix;
    }

    //////////////////////////////////////////////
    // COLOR
    //////////////////////////////////////////////
    /**
     * Basic fill material for geometry with a given color
     * @param  {Number|Number[]|String|p5.Color} v1  gray value,
     * red or hue value (depending on the current color mode),
     * or color Array, or CSS color string
     * @param  {Number}            [v2] green or saturation value
     * @param  {Number}            [v3] blue or brightness value
     * @param  {Number}            [a]  opacity
     * @chainable
     * @example
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     * }
     *
     * function draw() {
     *   background(0);
     *   noStroke();
     *   fill(100, 100, 240);
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.01);
     *   box(75, 75, 75);
     * }
     *
     * @alt
     * black canvas with purple cube spinning
     */
    fill(...args) {
      super.fill(...args);
      //see material.js for more info on color blending in webgl
      // const color = fn.color.apply(this._pInst, arguments);
      const color = this.states.fillColor;
      this.states.setValue('curFillColor', color._array);
      this.states.setValue('drawMode', FILL);
      this.states.setValue('_useNormalMaterial', false);
      this.states.setValue('_tex', null);
    }

    /**
     * Basic stroke material for geometry with a given color
     * @param  {Number|Number[]|String|p5.Color} v1  gray value,
     * red or hue value (depending on the current color mode),
     * or color Array, or CSS color string
     * @param  {Number}            [v2] green or saturation value
     * @param  {Number}            [v3] blue or brightness value
     * @param  {Number}            [a]  opacity
     * @example
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     * }
     *
     * function draw() {
     *   background(0);
     *   stroke(240, 150, 150);
     *   fill(100, 100, 240);
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.01);
     *   box(75, 75, 75);
     * }
     *
     * @alt
     * black canvas with purple cube with pink outline spinning
     */
    stroke(...args) {
      super.stroke(...args);
      // const color = fn.color.apply(this._pInst, arguments);
      this.states.setValue('curStrokeColor', this.states.strokeColor._array);
    }

    getCommonVertexProperties() {
      return {
        ...super.getCommonVertexProperties(),
        stroke: this.states.strokeColor,
        fill: this.states.fillColor,
        normal: this.states._currentNormal,
      };
    }

    getSupportedIndividualVertexProperties() {
      return {
        textureCoordinates: true,
      };
    }

    strokeCap(cap) {
      this.curStrokeCap = cap;
    }

    strokeJoin(join) {
      this.curStrokeJoin = join;
    }
    getFilterLayer() {
      if (!this.filterLayer) {
        this.filterLayer = new Framebuffer$1(this);
      }
      return this.filterLayer;
    }
    getFilterLayerTemp() {
      if (!this.filterLayerTemp) {
        this.filterLayerTemp = new Framebuffer$1(this);
      }
      return this.filterLayerTemp;
    }
    matchSize(fboToMatch, target) {
      if (
        fboToMatch.width !== target.width ||
        fboToMatch.height !== target.height
      ) {
        fboToMatch.resize(target.width, target.height);
      }

      if (fboToMatch.pixelDensity() !== target.pixelDensity()) {
        fboToMatch.pixelDensity(target.pixelDensity());
      }
    }
    filter(...args) {
      let fbo = this.getFilterLayer();

      // use internal shader for filter constants BLUR, INVERT, etc
      let filterParameter = undefined;
      let operation = undefined;
      if (typeof args[0] === 'string') {
        operation = args[0];
        let useDefaultParam =
          operation in filterParamDefaults && args[1] === undefined;
        filterParameter = useDefaultParam
          ? filterParamDefaults[operation]
          : args[1];

        // Create and store shader for constants once on initial filter call.
        // Need to store multiple in case user calls different filters,
        // eg. filter(BLUR) then filter(GRAY)
        if (!(operation in this.defaultFilterShaders)) {
          this.defaultFilterShaders[operation] = this._makeFilterShader(fbo.renderer, operation);
        }
        this.states.setValue(
          'filterShader',
          this.defaultFilterShaders[operation]
        );
      }
      // use custom user-supplied shader
      else {
        this.states.setValue('filterShader', args[0]);
      }

      // Setting the target to the framebuffer when applying a filter to a framebuffer.

      const target = this.activeFramebuffer() || this;

      // Resize the framebuffer 'fbo' and adjust its pixel density if it doesn't match the target.
      this.matchSize(fbo, target);

      fbo.draw(() => this.clear()); // prevent undesirable feedback effects accumulating secretly.

      let texelSize = [
        1 / (target.width * target.pixelDensity()),
        1 / (target.height * target.pixelDensity()),
      ];

      // apply blur shader with multiple passes.
      if (operation === BLUR) {
        // Treating 'tmp' as a framebuffer.
        const tmp = this.getFilterLayerTemp();
        // Resize the framebuffer 'tmp' and adjust its pixel density if it doesn't match the target.
        this.matchSize(tmp, target);
        // setup
        this.push();
        this.states.setValue('strokeColor', null);
        this.blendMode(BLEND);

        // draw main to temp buffer
        this.shader(this.states.filterShader);
        this.states.filterShader.setUniform('texelSize', texelSize);
        this.states.filterShader.setUniform('canvasSize', [
          target.width,
          target.height,
        ]);
        this.states.filterShader.setUniform(
          'radius',
          Math.max(1, filterParameter)
        );

        // Horiz pass: draw `target` to `tmp`
        tmp.draw(() => {
          this.states.filterShader.setUniform('direction', [1, 0]);
          this.states.filterShader.setUniform('tex0', target);
          this.clear();
          this.shader(this.states.filterShader);
          this.noLights();
          this.plane(target.width, target.height);
        });

        // Vert pass: draw `tmp` to `fbo`
        fbo.draw(() => {
          this.states.filterShader.setUniform('direction', [0, 1]);
          this.states.filterShader.setUniform('tex0', tmp);
          this.clear();
          this.shader(this.states.filterShader);
          this.noLights();
          this.plane(target.width, target.height);
        });

        this.pop();
      }
      // every other non-blur shader uses single pass
      else {
        fbo.draw(() => {
          this.states.setValue('strokeColor', null);
          this.blendMode(BLEND);
          this.shader(this.states.filterShader);
          this.states.filterShader.setUniform('tex0', target);
          this.states.filterShader.setUniform('texelSize', texelSize);
          this.states.filterShader.setUniform('canvasSize', [
            target.width,
            target.height,
          ]);
          // filterParameter uniform only used for POSTERIZE, and THRESHOLD
          // but shouldn't hurt to always set
          this.states.filterShader.setUniform('filterParameter', filterParameter);
          this.noLights();
          this.plane(target.width, target.height);
        });
      }
      // draw fbo contents onto main renderer.
      this.push();
      this.states.setValue('strokeColor', null);
      this.clear();
      this.push();
      this.states.setValue('imageMode', CORNER);
      this.blendMode(BLEND);
      target.filterCamera._resize();
      this.setCamera(target.filterCamera);
      this.resetMatrix();
      this._drawingFilter = true;
      this.image(
        fbo,
        0,
        0,
        fbo.width,
        fbo.height,
        -target.width / 2,
        -target.height / 2,
        target.width,
        target.height
      );
      this._drawingFilter = false;
      this.clearDepth();
      this.pop();
      this.pop();
    }

    // Pass this off to the host instance so that we can treat a renderer and a
    // framebuffer the same in filter()

    pixelDensity(newDensity) {
      if (newDensity) {
        return this._pInst.pixelDensity(newDensity);
      }
      return this._pInst.pixelDensity();
    }

    blendMode(mode) {
      if (
        mode === DARKEST ||
        mode === LIGHTEST ||
        mode === ADD ||
        mode === BLEND ||
        mode === SUBTRACT ||
        mode === SCREEN ||
        mode === EXCLUSION ||
        mode === REPLACE ||
        mode === MULTIPLY ||
        mode === REMOVE
      )
        this.states.setValue('curBlendMode', mode);
      else if (
        mode === BURN ||
        mode === OVERLAY ||
        mode === HARD_LIGHT ||
        mode === SOFT_LIGHT ||
        mode === DODGE
      ) {
        console.warn(
          'BURN, OVERLAY, HARD_LIGHT, SOFT_LIGHT, and DODGE only work for blendMode in 2D mode.'
        );
      }
    }

    erase(opacityFill, opacityStroke) {
      if (!this._isErasing) {
        this.preEraseBlend = this.states.curBlendMode;
        this._isErasing = true;
        this.blendMode(REMOVE);
        this._cachedFillStyle = this.states.curFillColor.slice();
        this.states.setValue('curFillColor', [1, 1, 1, opacityFill / 255]);
        this._cachedStrokeStyle = this.states.curStrokeColor.slice();
        this.states.setValue('curStrokeColor', [1, 1, 1, opacityStroke / 255]);
      }
    }

    noErase() {
      if (this._isErasing) {
        // Restore colors
        this.states.setValue('curFillColor', this._cachedFillStyle.slice());
        this.states.setValue('curStrokeColor', this._cachedStrokeStyle.slice());
        // Restore blend mode
        this.states.setValue('curBlendMode', this.preEraseBlend);
        this.blendMode(this.preEraseBlend);
        // Ensure that _applyBlendMode() sets preEraseBlend back to the original blend mode
        this._isErasing = false;
        this._applyBlendMode();
      }
    }

    _applyBlendMode() {
      // By default, a noop
    }

    drawTarget() {
      return this.activeFramebuffers[this.activeFramebuffers.length - 1] || this;
    }

    beginClip(options = {}) {
      super.beginClip(options);

      this.drawTarget()._isClipApplied = true;

      this._applyClip();

      this.push();
      this.resetShader();
      if (this.states.fillColor) this.fill(0, 0);
      if (this.states.strokeColor) this.stroke(0, 0);
    }

    endClip() {
      this.pop();

      this._unapplyClip();

      // Mark the depth at which the clip has been applied so that we can clear it
      // when we pop past this depth
      this._clipDepths.push(this._pushPopDepth);

      super.endClip();
    }

    _clearClip() {
      this._clearClipBuffer();
      if (this._clipDepths.length > 0) {
        this._clipDepths.pop();
      }
      this.drawTarget()._isClipApplied = false;
    }

    /**
     * @private
     * @returns {p5.Framebuffer} A p5.Framebuffer set to match the size and settings
     * of the renderer's canvas. It will be created if it does not yet exist, and
     * reused if it does.
     */
    _getTempFramebuffer() {
      if (!this._tempFramebuffer) {
        this._tempFramebuffer = new Framebuffer$1(this, {
          format: UNSIGNED_BYTE,
          useDepth: this._pInst._glAttributes.depth,
          depthFormat: UNSIGNED_INT,
          antialias: this._pInst._glAttributes.antialias,
        });
      }
      return this._tempFramebuffer;
    }

    //////////////////////////////////////////////
    // HASH | for geometry
    //////////////////////////////////////////////

    geometryInHash(gid) {
      return this.geometryBufferCache.isCached(gid);
    }

    /**
     * [resize description]
     * @private
     * @param  {Number} w [description]
     * @param  {Number} h [description]
     */
    resize(w, h) {
      super.resize(w, h);

      // save canvas properties
      const props = {};
      for (const key in this.drawingContext) {
        const val = this.drawingContext[key];
        if (typeof val !== "object" && typeof val !== "function") {
          props[key] = val;
        }
      }

      const dimensions = this._adjustDimensions(w, h);
      w = dimensions.adjustedWidth;
      h = dimensions.adjustedHeight;

      this.width = w;
      this.height = h;

      this.canvas.width = w * this._pixelDensity;
      this.canvas.height = h * this._pixelDensity;
      this.canvas.style.width = `${w}px`;
      this.canvas.style.height = `${h}px`;
      this._updateViewport();
      this._updateSize();

      this.mainCamera._resize();
      if (this.states.curCamera !== this.mainCamera) {
        this.states.curCamera._resize();
      }

      //resize pixels buffer
      if (typeof this.pixels !== "undefined") {
        this._createPixelsArray();
      }

      for (const framebuffer of this.framebuffers) {
        // Notify framebuffers of the resize so that any auto-sized framebuffers
        // can also update their size
        this.flushDraw?.();
        framebuffer._canvasSizeChanged();
      }
      this.flushDraw?.();

      // reset canvas properties
      for (const savedKey in props) {
        try {
          this.drawingContext[savedKey] = props[savedKey];
        } catch (err) {
          // ignore read-only property errors
        }
      }
    }

    applyMatrix(a, b, c, d, e, f) {
      this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone());
      if (arguments.length === 16) {
        // this.states.uModelMatrix.apply(arguments);
        Matrix.prototype.apply.apply(this.states.uModelMatrix, arguments);
      } else {
        this.states.uModelMatrix.apply([
          a,
          b,
          0,
          0,
          c,
          d,
          0,
          0,
          0,
          0,
          1,
          0,
          e,
          f,
          0,
          1,
        ]);
      }
    }

    /**
     * [translate description]
     * @private
     * @param  {Number} x [description]
     * @param  {Number} y [description]
     * @param  {Number} z [description]
     * @chainable
     * @todo implement handle for components or vector as args
     */
    translate(x, y, z) {
      if (x instanceof Vector) {
        z = x.z;
        y = x.y;
        x = x.x;
      }
      this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone());
      this.states.uModelMatrix.translate([x, y, z]);
      return this;
    }

    /**
     * Scales the Model View Matrix by a vector
     * @private
     * @param  {Number | p5.Vector | Array} x [description]
     * @param  {Number} [y] y-axis scalar
     * @param  {Number} [z] z-axis scalar
     * @chainable
     */
    scale(x, y, z) {
      this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone());
      this.states.uModelMatrix.scale(x, y, z);
      return this;
    }

    rotate(rad, axis) {
      if (typeof axis === "undefined") {
        return this.rotateZ(rad);
      }
      this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone());
      Matrix.prototype.rotate4x4.apply(this.states.uModelMatrix, arguments);
      return this;
    }

    rotateX(rad) {
      this.rotate(rad, 1, 0, 0);
      return this;
    }

    rotateY(rad) {
      this.rotate(rad, 0, 1, 0);
      return this;
    }

    rotateZ(rad) {
      this.rotate(rad, 0, 0, 1);
      return this;
    }

    pop(...args) {
      if (
        this._clipDepths.length > 0 &&
        this._pushPopDepth === this._clipDepths[this._clipDepths.length - 1]
      ) {
        this._clearClip();
      }
      super.pop(...args);
      this._applyStencilTestIfClipping();
    }

    resetMatrix() {
      this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone());
      this.states.uModelMatrix.reset();
      this.states.setValue("uViewMatrix", this.states.uViewMatrix.clone());
      this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix);
      return this;
    }

    //////////////////////////////////////////////
    // SHADER
    //////////////////////////////////////////////

    _getStrokeShader() {
      // select the stroke shader to use
      const stroke = this.states.userStrokeShader;
      if (stroke) {
        return stroke;
      }
      return this._getLineShader();
    }

    /*
     * This method will handle both image shaders and
     * fill shaders, returning the appropriate shader
     * depending on the current context (image or shape).
     */
    _getFillShader() {
      // If drawing an image, check for user-defined image shader and filters
      if (this._drawingImage) {
        // Use user-defined image shader if available and no filter is applied
        if (this.states.userImageShader && !this._drawingFilter) {
          return this.states.userImageShader;
        } else {
          return this._getLightShader(); // Fallback to light shader
        }
      }
      // If user has defined a fill shader, return that
      else if (this.states.userFillShader) {
        return this.states.userFillShader;
      }
      // Use normal shader if normal material is active
      else if (this.states._useNormalMaterial) {
        return this._getNormalShader();
      }
      // Use light shader if lighting or textures are enabled
      else if (this.states.enableLighting || this.states._tex) {
        return this._getLightShader();
      }
      // Default to color shader if no other conditions are met
      return this._getColorShader();
    }

    baseMaterialShader() {
      return this._getLightShader();
    }

    baseNormalShader() {
      return this._getNormalShader();
    }

    baseColorShader() {
      return this._getColorShader();
    }

    baseStrokeShader() {
      return this._getLineShader();
    }

    /**
     * @private
     * @returns {p5.Framebuffer|null} The currently active framebuffer, or null if
     * the main canvas is the current draw target.
     */
    activeFramebuffer() {
      return this.activeFramebuffers[this.activeFramebuffers.length - 1] || null;
    }

    createFramebuffer(options) {
      return new Framebuffer$1(this, options);
    }

    _setGlobalUniforms(shader) {
      const modelMatrix = this.states.uModelMatrix;
      const viewMatrix = this.states.uViewMatrix;
      const projectionMatrix = this.states.uPMatrix;
      const modelViewMatrix = modelMatrix.copy().mult(viewMatrix);

      shader.setUniform(
        "uPerspective",
        this.states.curCamera.useLinePerspective ? 1 : 0
      );
      shader.setUniform("uViewMatrix", viewMatrix.mat4);
      shader.setUniform("uProjectionMatrix", projectionMatrix.mat4);
      shader.setUniform("uModelMatrix", modelMatrix.mat4);
      shader.setUniform("uModelViewMatrix", modelViewMatrix.mat4);
      if (shader.uniforms.uModelViewProjectionMatrix) {
        const modelViewProjectionMatrix = modelViewMatrix.copy();
        modelViewProjectionMatrix.mult(projectionMatrix);
        shader.setUniform(
          "uModelViewProjectionMatrix",
          modelViewProjectionMatrix.mat4
        );
      }
      if (shader.uniforms.uNormalMatrix) {
        this.scratchMat3.inverseTranspose4x4(modelViewMatrix);
        shader.setUniform("uNormalMatrix", this.scratchMat3.mat3);
      }
      if (shader.uniforms.uModelNormalMatrix) {
        this.scratchMat3.inverseTranspose4x4(this.states.uModelMatrix);
        shader.setUniform("uModelNormalMatrix", this.scratchMat3.mat3);
      }
      if (shader.uniforms.uCameraNormalMatrix) {
        this.scratchMat3.inverseTranspose4x4(this.states.uViewMatrix);
        shader.setUniform("uCameraNormalMatrix", this.scratchMat3.mat3);
      }
      shader.setUniform("uViewport", this._viewport);
    }

    _setStrokeUniforms(strokeShader) {
      // set the uniform values
      strokeShader.setUniform("uSimpleLines", this._simpleLines);
      strokeShader.setUniform("uUseLineColor", this._useLineColor);
      strokeShader.setUniform("uMaterialColor", this.states.curStrokeColor);
      strokeShader.setUniform("uStrokeWeight", this.states.strokeWeight);
      strokeShader.setUniform("uStrokeCap", STROKE_CAP_ENUM[this.curStrokeCap]);
      strokeShader.setUniform(
        "uStrokeJoin",
        STROKE_JOIN_ENUM[this.curStrokeJoin]
      );
    }

    _setFillUniforms(fillShader) {
      this.mixedSpecularColor = [...this.states.curSpecularColor];
      const empty = this._getEmptyTexture();

      if (this.states._useMetalness > 0) {
        this.mixedSpecularColor = this.mixedSpecularColor.map(
          (mixedSpecularColor, index) =>
            this.states.curFillColor[index] * this.states._useMetalness +
            mixedSpecularColor * (1 - this.states._useMetalness)
        );
      }

      // TODO: optimize
      fillShader.setUniform("uUseVertexColor", this._useVertexColor);
      fillShader.setUniform("uMaterialColor", this.states.curFillColor);
      fillShader.setUniform("isTexture", !!this.states._tex);
      // We need to explicitly set uSampler back to an empty texture here.
      // In general, we record the last set texture so we can re-apply it
      // the next time a shader is used. However, the texture() function
      // works differently and is global p5 state. If the p5 state has
      // been cleared, we also need to clear the value in uSampler to match.
      fillShader.setUniform("uSampler", this.states._tex || empty);
      fillShader.setUniform("uTint", this.states.tint);

      fillShader.setUniform("uHasSetAmbient", this.states._hasSetAmbient);
      fillShader.setUniform("uAmbientMatColor", this.states.curAmbientColor);
      fillShader.setUniform("uSpecularMatColor", this.mixedSpecularColor);
      fillShader.setUniform("uEmissiveMatColor", this.states.curEmissiveColor);
      fillShader.setUniform("uSpecular", this.states._useSpecularMaterial);
      fillShader.setUniform("uEmissive", this.states._useEmissiveMaterial);
      fillShader.setUniform("uShininess", this.states._useShininess);
      fillShader.setUniform("uMetallic", this.states._useMetalness);

      this._setImageLightUniforms(fillShader);

      fillShader.setUniform("uUseLighting", this.states.enableLighting);

      const pointLightCount = this.states.pointLightDiffuseColors.length / 3;
      fillShader.setUniform("uPointLightCount", pointLightCount);
      fillShader.setUniform(
        "uPointLightLocation",
        this.states.pointLightPositions
      );
      fillShader.setUniform(
        "uPointLightDiffuseColors",
        this.states.pointLightDiffuseColors
      );
      fillShader.setUniform(
        "uPointLightSpecularColors",
        this.states.pointLightSpecularColors
      );

      const directionalLightCount =
        this.states.directionalLightDiffuseColors.length / 3;
      fillShader.setUniform("uDirectionalLightCount", directionalLightCount);
      fillShader.setUniform(
        "uLightingDirection",
        this.states.directionalLightDirections
      );
      fillShader.setUniform(
        "uDirectionalDiffuseColors",
        this.states.directionalLightDiffuseColors
      );
      fillShader.setUniform(
        "uDirectionalSpecularColors",
        this.states.directionalLightSpecularColors
      );

      // TODO: sum these here...
      let mixedAmbientLight = [0, 0, 0];
      for (let i = 0; i < this.states.ambientLightColors.length; i += 3) {
        for (let off = 0; off < 3; off++) {
          if (this.states._useMetalness > 0) {
            mixedAmbientLight[off] += Math.max(
              0,
              this.states.ambientLightColors[i + off] - this.states._useMetalness
            );
          } else {
            mixedAmbientLight[off] += this.states.ambientLightColors[i + off];
          }
        }
      }
      fillShader.setUniform("uAmbientColor", mixedAmbientLight);

      const spotLightCount = this.states.spotLightDiffuseColors.length / 3;
      fillShader.setUniform("uSpotLightCount", spotLightCount);
      fillShader.setUniform("uSpotLightAngle", this.states.spotLightAngle);
      fillShader.setUniform("uSpotLightConc", this.states.spotLightConc);
      fillShader.setUniform(
        "uSpotLightDiffuseColors",
        this.states.spotLightDiffuseColors
      );
      fillShader.setUniform(
        "uSpotLightSpecularColors",
        this.states.spotLightSpecularColors
      );
      fillShader.setUniform("uSpotLightLocation", this.states.spotLightPositions);
      fillShader.setUniform(
        "uSpotLightDirection",
        this.states.spotLightDirections
      );

      fillShader.setUniform(
        "uConstantAttenuation",
        this.states.constantAttenuation
      );
      fillShader.setUniform("uLinearAttenuation", this.states.linearAttenuation);
      fillShader.setUniform(
        "uQuadraticAttenuation",
        this.states.quadraticAttenuation
      );
    }

    // getting called from _setFillUniforms
    _setImageLightUniforms(shader) {
      //set uniform values
      shader.setUniform("uUseImageLight", this.states.activeImageLight != null);
      // true
      if (this.states.activeImageLight) {
        // this.states.activeImageLight has image as a key
        // look up the texture from the diffusedTexture map
        let diffusedLight = this.getDiffusedTexture(this.states.activeImageLight);
        shader.setUniform("environmentMapDiffused", diffusedLight);
        let specularLight = this.getSpecularTexture(this.states.activeImageLight);

        shader.setUniform("environmentMapSpecular", specularLight);
      } else {
        shader.setUniform("environmentMapDiffused", this._getEmptyTexture());
        shader.setUniform("environmentMapSpecular", this._getEmptyTexture());
      }
    }

    /**
     * @private
     * Note: DO NOT CALL THIS while in the middle of binding another texture,
     * since it will change the texture binding in order to allocate the empty
     * texture! Grab its value beforehand!
     */
    _getEmptyTexture() {
      if (!this._emptyTexture) {
        // a plain white texture RGBA, full alpha, single pixel.
        const im = new Image(1, 1);
        im.set(0, 0, 255);
        this._emptyTexture = new Texture(this, im);
      }
      return this._emptyTexture;
    }

    getTexture(input) {
      let src = input;
      if (src instanceof Framebuffer$1) {
        src = src.color;
      }

      const texture = this.textures.get(src);
      if (texture) {
        return texture;
      }

      const tex = new Texture(this, src);
      this.textures.set(src, tex);
      return tex;
    }

    //////////////////////////////////////////////
    // Buffers
    //////////////////////////////////////////////

    _normalizeBufferData(values, type = Float32Array) {
      if (!values) return null;
      if (values instanceof DataArray) {
        return values.dataArray();
      }
      if (values instanceof type) {
        return values;
      }
      return new type(values);
    }

    ///////////////////////////////
    //// UTILITY FUNCTIONS
    //////////////////////////////
    _arraysEqual(a, b) {
      const aLength = a.length;
      if (aLength !== b.length) return false;
      return a.every((ai, i) => ai === b[i]);
    }

    _isTypedArray(arr) {
      return [
        Float32Array,
        Float64Array,
        Int16Array,
        Uint16Array,
        Uint32Array,
      ].some((x) => arr instanceof x);
    }

    /**
     * turn a p5.Vector Array into a one dimensional number array
     * @private
     * @param  {p5.Vector[]} arr  an array of p5.Vector
     * @return {Number[]}     a one dimensional array of numbers
     * [p5.Vector(1, 2, 3), p5.Vector(4, 5, 6)] ->
     * [1, 2, 3, 4, 5, 6]
     */
    _vToNArray(arr) {
      return arr.flatMap((item) => [item.x, item.y, item.z]);
    }

    ///////////////////////////////
    //// TEXT SUPPORT METHODS
    //////////////////////////////

    _beforeDrawText() {}
    _afterDrawText() {}

    textCanvas() {
      if (!this._textCanvas) {
        this._textCanvas = document.createElement('canvas');
        this._textCanvas.width = 1;
        this._textCanvas.height = 1;
        this._textCanvas.style.display = 'none';
        // Has to be added to the DOM for measureText to work properly!
        this.canvas.parentElement.insertBefore(this._textCanvas, this.canvas);
      }
      return this._textCanvas;
    }

    textDrawingContext() {
      if (!this._textDrawingContext) {
        const textCanvas = this.textCanvas();
        this._textDrawingContext = textCanvas.getContext('2d');
      }
      return this._textDrawingContext;
    }

    _positionLines(x, y, width, height, lines) {
      let { textLeading, textAlign } = this.states;
      const widths = lines.map(line => this._fontWidthSingle(line));
      let adjustedX, lineData = new Array(lines.length);
      let adjustedW = typeof width === 'undefined' ? Math.max(0, ...widths) : width;
      let adjustedH = typeof height === 'undefined' ? 0 : height;

      for (let i = 0; i < lines.length; i++) {
        switch (textAlign) {
          case textCoreConstants.START:
            throw new Error('textBounds: START not yet supported for textAlign'); // default to LEFT
          case LEFT:
            adjustedX = x;
            break;
          case CENTER:
            adjustedX = x +
              (adjustedW - widths[i]) / 2 -
              adjustedW / 2 +
              (width || 0) / 2;
            break;
          case RIGHT:
            adjustedX = x + adjustedW - widths[i] - adjustedW + (width || 0);
            break;
          case textCoreConstants.END:
            throw new Error('textBounds: END not yet supported for textAlign');
          default:
            adjustedX = x;
            break;
        }
        lineData[i] = { text: lines[i], x: adjustedX, y: y + i * textLeading };
      }

      return this._yAlignOffset(lineData, adjustedH);
    }

    _verticalAlignFont = function() {
      const ctx = this.textDrawingContext();
      const metrics = ctx.measureText('X');
      return -metrics.alphabeticBaseline ||
        (-metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent);
    }

    _yAlignOffset(dataArr, height) {
      if (typeof height === 'undefined') {
        throw Error('_yAlignOffset: height is required');
      }

      let { textLeading, textBaseline, textSize} = this.states;
      let yOff = 0, numLines = dataArr.length;
      let totalHeight = textSize * numLines +
        ((textLeading - textSize) * (numLines - 1));
      switch (textBaseline) { // drawingContext ?
        case TOP:
          yOff = this._verticalAlignFont();
          break;
        case BASELINE:
          break;
        case textCoreConstants._CTX_MIDDLE:
          yOff = (-totalHeight + textSize + (height || 0)) / 2 + this._verticalAlignFont() + this._middleAlignOffset();
          break;
        case BOTTOM:
          yOff = -(totalHeight - textSize) + (height || 0);
          break;
        default:
          console.warn(`${textBaseline} is not supported in WebGL mode.`); // FES?
          break;
      }
      dataArr.forEach(ele => ele.y += yOff);
      return dataArr;
    }

    _makeFilterShader(renderer, operation) {
      const p5 = this._pInst;

      return makeFilterShader(this, operation, p5);
    }

    /*
     * As part of imageLight(): we need to create a texture representing
     * the diffused light hitting an object from each angle. This will
     * accumulate light from angles in a hemisphere, weighted according to
     * how head-on the light angle is.
     *
     * This method returns a p5.Framebuffer that stores these values, mapping
     * an angle to each pixel. This creates and caches textures for reuse, since
     * creating this texture is somewhat expensive.
     */
    makeDiffusedTexture(input) {
      // if one already exists for a given input image
      if (this.diffusedTextures.get(input) != null) {
        return this.diffusedTextures.get(input);
      }
      // if not, only then create one
      let newFramebuffer;
      // hardcoded to 200px, because it's going to be blurry and smooth
      let smallWidth = 200;
      let width = smallWidth;
      let height = Math.floor(smallWidth * (input.height / input.width));
      newFramebuffer = new Framebuffer$1(this, {
        width,
        height,
        density: 1,
      });
      // create framebuffer is like making a new sketch, all functions on main
      // sketch it would be available on framebuffer
      if (!this.diffusedShader) {
        this.diffusedShader = this._createImageLightShader("diffused");
      }
      newFramebuffer.draw(() => {
        this.shader(this.diffusedShader);
        this._setImageLightShaderUniforms(this.diffusedShader, input);
        this.states.setValue("strokeColor", null);
        this.noLights();
        this.plane(width, height);
      });
      this.diffusedTextures.set(input, newFramebuffer);
      return newFramebuffer;
    }
    getDiffusedTexture(input) {
      return this.diffusedTextures.get(input);
    }

    /*
     * used in imageLight,
     * To create a texture from the input non blurry image, if it doesn't already exist
     * Creating 8 different levels of textures according to different
     * sizes and storing them in `levels` array
     * Creating a new Mipmap texture with that `levels` array
     * Storing the texture for input image in map called `specularTextures`
     * maps the input Image to a p5.MipmapTexture
     */
    makeSpecularTexture(input) {
      // check if already exits (there are tex of diff resolution so which one to check)
      // currently doing the whole array
      if (this.specularTextures.get(input) != null) {
        return this.specularTextures.get(input);
      }
      // Hardcoded size
      const size = 512;
      let tex;
      let count = Math.floor(Math.log2(size)) + 1; // Actual number of mip levels from size down to 1x1

      if (!this.specularShader) {
        this.specularShader = this._createImageLightShader("specular");
      }

      // Prepare mipmap level accumulator
      const mipmapData = this._prepareMipmapData(size, count);

      const framebuffer = new Framebuffer$1(this, {
        width: size,
        height: size,
        density: 1,
      });

      // currently only 8 levels
      // This loop calculates 8 framebuffers of varying size of canvas
      // and corresponding different roughness levels.
      // Roughness increases with the decrease in canvas size,
      // because rougher surfaces have less detailed/more blurry reflections.
      let mipLevel = 0;
      for (let w = size; w >= 1; w /= 2) {
        framebuffer.resize(w, w);
        let currCount = Math.log(w) / Math.log(2);
        let roughness = 1 - currCount / count;
        framebuffer.draw(() => {
          this.shader(this.specularShader);
          this.clear();
          this._setImageLightShaderUniforms(
            this.specularShader,
            input,
            roughness,
          );
          this.states.setValue("strokeColor", null);
          this.noLights();
          this.plane(w, w);
        });

        // Accumulate framebuffer content for this mip level
        this._accumulateMipLevel(framebuffer, mipmapData, mipLevel, w, w);
        mipLevel++;
      }

      // Free the Framebuffer
      framebuffer.remove();

      // Create the final MipmapTexture from accumulated data
      tex = this._finalizeMipmapTexture(mipmapData);
      this.specularTextures.set(input, tex);
      return tex;
    }
    getSpecularTexture(input) {
      return this.specularTextures.get(input);
    }

    _getSphereMapping(img) {
      if (!this.sphereMapping) {
        const p5 = this._pInst;
        this.sphereMapping = this.baseFilterShader().modify(({ p5 }) => {
          const uEnvMap = p5.uniformTexture('uEnvMap');
          const uFovY = p5.uniformFloat('uFovY');
          const uAspect = p5.uniformFloat('uAspect');
          // Hack: we don't have matrix uniforms yet; use three vectors
          const uN1 = p5.uniformVec3('uN1');
          const uN2 = p5.uniformVec3('uN2');
          const uN3 = p5.uniformVec3('uN3');
          p5.getColor((inputs) => {
            const uFovX = uFovY * uAspect;
            const angleY = p5.mix(uFovY/2.0,  -uFovY/2.0, inputs.texCoord.y);
            const angleX = p5.mix(uFovX/2.0, -uFovX/2.0, inputs.texCoord.x);
            let rotatedNormal = p5.normalize([angleX, angleY, 1]);
            rotatedNormal = [
              // Don't mind me, just doing matrix vector multiplication...
              p5.dot(rotatedNormal, uN1),
              p5.dot(rotatedNormal, uN2),
              p5.dot(rotatedNormal, uN3),
            ];
            const temp = rotatedNormal.z;
            rotatedNormal.z = rotatedNormal.x;
            rotatedNormal.x = -temp;
            const suv = [
              p5.atan(rotatedNormal.z, rotatedNormal.x) / (2.0 * p5.PI) + 0.5,
              0.5 + 0.5 * (-rotatedNormal.y)
            ];
            return p5.getTexture(uEnvMap, suv);
          });
        }, { p5 });
      }
      this.scratchMat3.inverseTranspose4x4(this.states.uViewMatrix);
      this.scratchMat3.invert(this.scratchMat3); // uNMMatrix is 3x3
      this.sphereMapping.setUniform("uFovY", this.states.curCamera.cameraFOV);
      this.sphereMapping.setUniform("uAspect", this.states.curCamera.aspectRatio);
      // Pass in the normal matrix as three vectors. TODO replace this with
      // an actual matrix uniform once we have those again.
      const m = this.scratchMat3.mat3;
      this.sphereMapping.setUniform("uN1", [m[0], m[3], m[6]]);
      this.sphereMapping.setUniform("uN2", [m[1], m[4], m[7]]);
      this.sphereMapping.setUniform("uN3", [m[2], m[5], m[8]]);
      this.sphereMapping.setUniform("uEnvMap", img);
      return this.sphereMapping;
    }

    /*
     * Abstract methods to be implemented by specific renderers
     */
    _createImageLightShader(type) {
      throw new Error(
        "_createImageLightShader must be implemented by the renderer",
      );
    }

    _setImageLightShaderUniforms(shader, input, roughness) {
      shader.setUniform("environmentMap", input);
      if (roughness !== undefined) {
        shader.setUniform("roughness", roughness);
      }
    }

    _createMipmapTexture(levels) {
      throw new Error("_createMipmapTexture must be implemented by the renderer");
    }

    _prepareMipmapData(size, mipLevels) {
      throw new Error("_prepareMipmapData must be implemented by the renderer");
    }

    _accumulateMipLevel(framebuffer, mipmapData, mipLevel, width, height) {
      throw new Error("_accumulateMipLevel must be implemented by the renderer");
    }

    _finalizeMipmapTexture(mipmapData) {
      throw new Error(
        "_finalizeMipmapTexture must be implemented by the renderer",
      );
    }

    remove() {
      if (this._textCanvas) {
        this._textCanvas.parentElement.removeChild(this._textCanvas);
      }
      super.remove();
    }
  }

  function renderer3D(p5, fn) {
    p5.Renderer3D = Renderer3D;
  }

  if (typeof p5 !== "undefined") {
    renderer3D(p5);
  }

  /**
   * @module Shape
   * @submodule 3D Primitives
   * @for p5
   * @requires core
   * @requires p5.Geometry
   */


  function primitives3D(p5, fn){
    /**
     * Sets the stroke rendering mode to balance performance and visual features when drawing lines.
     *
     * `strokeMode()` offers two modes:
     *
     * - `SIMPLE`: Optimizes for speed by disabling caps, joins, and stroke color features.
     *   Use this mode for faster line rendering when these visual details are unnecessary.
     * - `FULL`: Enables caps, joins, and stroke color for lines.
     *   This mode provides enhanced visuals but may reduce performance due to additional processing.
     *
     * Choose the mode that best suits your application's needs to either improve rendering speed or enhance visual quality.
     *
     * @method strokeMode
     * @param {String} mode - The stroke mode to set. Possible values are:
     *   - `'SIMPLE'`: Fast rendering without caps, joins, or stroke color.
     *   - `'FULL'`: Detailed rendering with caps, joins, and stroke color.
     *
     * @example
     * function setup() {
     *   createCanvas(300, 300, WEBGL);
     *   describe('A sphere with red stroke and a red, wavy line on a gray background. The wavy line have caps, joins and colors.');
     * }
     *
     * function draw() {
     *   background(128);
     *   strokeMode(FULL); // Enables detailed rendering with caps, joins, and stroke color.
     *   push();
     *   strokeWeight(1);
     *   translate(0, -50, 0);
     *   sphere(50);
     *   pop();
     *   orbitControl();
     *
     *   noFill();
     *   strokeWeight(15);
     *   stroke('red');
     *   beginShape();
     *   bezierOrder(2); // Sets the order of the Bezier curve.
     *   bezierVertex(80, 80);
     *   bezierVertex(50, -40);
     *   bezierVertex(-80, 80);
     *   endShape();
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(300, 300, WEBGL);
     *   describe('A sphere with red stroke and a  wavy line without full curve decorations without caps and color on a gray background.');
     * }
     *
     * function draw() {
     *   background(128);
     *   strokeMode(SIMPLE); // Simplifies stroke rendering for better performance.
     *
     *   // Draw sphere
     *   push();
     *   strokeWeight(1);
     *   translate(0, -50, 0);
     *   sphere(50);
     *   pop();
     *   orbitControl();
     *
     *   // Draw modified wavy red line
     *   noFill();
     *   strokeWeight(15);
     *   stroke('red');
     *   beginShape();
     *   bezierOrder(2); // Sets the order of the Bezier curve.
     *   bezierVertex(80, 80);
     *   bezierVertex(50, -40);
     *   bezierVertex(-80, 80);
     *   endShape();
     * }
     */
    fn.strokeMode = function (mode) {
      if (mode === undefined) {
        return this._renderer._simpleLines ? SIMPLE : FULL;
      } else if (mode === SIMPLE) {
        this._renderer._simpleLines = true;
      } else if (mode === FULL) {
        this._renderer._simpleLines = false;
      } else {
        throw Error('no such parameter');
      }
    };

    /**
     * Creates a custom <a href="#/p5.Geometry">p5.Geometry</a> object from
     * simpler 3D shapes.
     *
     * `buildGeometry()` helps with creating complex 3D shapes from simpler ones
     * such as <a href="#/p5/sphere">sphere()</a>. It can help to make sketches
     * more performant. For example, if a complex 3D shape doesn’t change while a
     * sketch runs, then it can be created with `buildGeometry()`. Creating a
     * <a href="#/p5.Geometry">p5.Geometry</a> object once and then drawing it
     * will run faster than repeatedly drawing the individual pieces.
     *
     * The parameter, `callback`, is a function with the drawing instructions for
     * the new <a href="#/p5.Geometry">p5.Geometry</a> object. It will be called
     * once to create the new 3D shape.
     *
     * See <a href="#/p5/beginGeometry">beginGeometry()</a> and
     * <a href="#/p5/endGeometry">endGeometry()</a> for another way to build 3D
     * shapes.
     *
     * Note: `buildGeometry()` can only be used in WebGL mode.
     *
     * @method buildGeometry
     * @param {Function} callback function that draws the shape.
     * @returns {p5.Geometry} new 3D shape.
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let shape;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the p5.Geometry object.
     *   shape = buildGeometry(createShape);
     *
     *   describe('A white cone drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Style the p5.Geometry object.
     *   noStroke();
     *
     *   // Draw the p5.Geometry object.
     *   model(shape);
     * }
     *
     * // Create p5.Geometry object from a single cone.
     * function createShape() {
     *   cone();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let shape;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the arrow.
     *   shape = buildGeometry(createArrow);
     *
     *   describe('A white arrow drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Style the arrow.
     *   noStroke();
     *
     *   // Draw the arrow.
     *   model(shape);
     * }
     *
     * function createArrow() {
     *   // Add shapes to the p5.Geometry object.
     *   push();
     *   rotateX(PI);
     *   cone(10);
     *   translate(0, -10, 0);
     *   cylinder(3, 20);
     *   pop();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let shape;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the p5.Geometry object.
     *   shape = buildGeometry(createArrow);
     *
     *   describe('Two white arrows drawn on a gray background. The arrow on the right rotates slowly.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Style the arrows.
     *   noStroke();
     *
     *   // Draw the p5.Geometry object.
     *   model(shape);
     *
     *   // Translate and rotate the coordinate system.
     *   translate(30, 0, 0);
     *   rotateZ(frameCount * 0.01);
     *
     *   // Draw the p5.Geometry object again.
     *   model(shape);
     * }
     *
     * function createArrow() {
     *   // Add shapes to the p5.Geometry object.
     *   push();
     *   rotateX(PI);
     *   cone(10);
     *   translate(0, -10, 0);
     *   cylinder(3, 20);
     *   pop();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let button;
     * let particles;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a button to reset the particle system.
     *   button = createButton('Reset');
     *
     *   // Call resetModel() when the user presses the button.
     *   button.mousePressed(resetModel);
     *
     *   // Add the original set of particles.
     *   resetModel();
     *
     *   describe('A set of white spheres on a gray background. The spheres are positioned randomly. Their positions reset when the user presses the Reset button.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Style the particles.
     *   noStroke();
     *
     *   // Draw the particles.
     *   model(particles);
     * }
     *
     * function resetModel() {
     *   // If the p5.Geometry object has already been created,
     *   // free those resources.
     *   if (particles) {
     *     freeGeometry(particles);
     *   }
     *
     *   // Create a new p5.Geometry object with random spheres.
     *   particles = buildGeometry(createParticles);
     * }
     *
     * function createParticles() {
     *   for (let i = 0; i < 60; i += 1) {
     *     // Calculate random coordinates.
     *     let x = randomGaussian(0, 20);
     *     let y = randomGaussian(0, 20);
     *     let z = randomGaussian(0, 20);
     *
     *     push();
     *     // Translate to the particle's coordinates.
     *     translate(x, y, z);
     *     // Draw the particle.
     *     sphere(5);
     *     pop();
     *   }
     * }
     */
    fn.buildGeometry = function(callback) {
      return this._renderer.buildGeometry(callback);
    };

    /**
     * Clears a <a href="#/p5.Geometry">p5.Geometry</a> object from the graphics
     * processing unit (GPU) memory.
     *
     * <a href="#/p5.Geometry">p5.Geometry</a> objects can contain lots of data
     * about their vertices, surface normals, colors, and so on. Complex 3D shapes
     * can use lots of memory which is a limited resource in many GPUs. Calling
     * `freeGeometry()` can improve performance by freeing a
     * <a href="#/p5.Geometry">p5.Geometry</a> object’s resources from GPU memory.
     * `freeGeometry()` works with <a href="#/p5.Geometry">p5.Geometry</a> objects
     * created with <a href="#/p5/beginGeometry">beginGeometry()</a> and
     * <a href="#/p5/endGeometry">endGeometry()</a>,
     * <a href="#/p5/buildGeometry">buildGeometry()</a>, and
     * <a href="#/p5/loadModel">loadModel()</a>.
     *
     * The parameter, `geometry`, is the <a href="#/p5.Geometry">p5.Geometry</a>
     * object to be freed.
     *
     * Note: A <a href="#/p5.Geometry">p5.Geometry</a> object can still be drawn
     * after its resources are cleared from GPU memory. It may take longer to draw
     * the first time it’s redrawn.
     *
     * Note: `freeGeometry()` can only be used in WebGL mode.
     *
     * @method freeGeometry
     * @param {p5.Geometry} geometry 3D shape whose resources should be freed.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   // Create a p5.Geometry object.
     *   beginGeometry();
     *   cone();
     *   let shape = endGeometry();
     *
     *   // Draw the shape.
     *   model(shape);
     *
     *   // Free the shape's resources.
     *   freeGeometry(shape);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let button;
     * let particles;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a button to reset the particle system.
     *   button = createButton('Reset');
     *
     *   // Call resetModel() when the user presses the button.
     *   button.mousePressed(resetModel);
     *
     *   // Add the original set of particles.
     *   resetModel();
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Style the particles.
     *   noStroke();
     *
     *   // Draw the particles.
     *   model(particles);
     * }
     *
     * function resetModel() {
     *   // If the p5.Geometry object has already been created,
     *   // free those resources.
     *   if (particles) {
     *     freeGeometry(particles);
     *   }
     *
     *   // Create a new p5.Geometry object with random spheres.
     *   particles = buildGeometry(createParticles);
     * }
     *
     * function createParticles() {
     *   for (let i = 0; i < 60; i += 1) {
     *     // Calculate random coordinates.
     *     let x = randomGaussian(0, 20);
     *     let y = randomGaussian(0, 20);
     *     let z = randomGaussian(0, 20);
     *
     *     push();
     *     // Translate to the particle's coordinates.
     *     translate(x, y, z);
     *     // Draw the particle.
     *     sphere(5);
     *     pop();
     *   }
     * }
     */
    fn.freeGeometry = function(geometry) {
      this._renderer.geometryBufferCache.freeBuffers(geometry.gid);
    };

    /**
     * Draws a plane.
     *
     * A plane is a four-sided, flat shape with every angle measuring 90˚. It’s
     * similar to a rectangle and offers advanced drawing features in WebGL mode.
     *
     * The first parameter, `width`, is optional. If a `Number` is passed, as in
     * `plane(20)`, it sets the plane’s width and height. By default, `width` is
     * 50.
     *
     * The second parameter, `height`, is also optional. If a `Number` is passed,
     * as in `plane(20, 30)`, it sets the plane’s height. By default, `height` is
     * set to the plane’s `width`.
     *
     * The third parameter, `detailX`, is also optional. If a `Number` is passed,
     * as in `plane(20, 30, 5)` it sets the number of triangle subdivisions to use
     * along the x-axis. All 3D shapes are made by connecting triangles to form
     * their surfaces. By default, `detailX` is 1.
     *
     * The fourth parameter, `detailY`, is also optional. If a `Number` is passed,
     * as in `plane(20, 30, 5, 7)` it sets the number of triangle subdivisions to
     * use along the y-axis. All 3D shapes are made by connecting triangles to
     * form their surfaces. By default, `detailY` is 1.
     *
     * Note: `plane()` can only be used in WebGL mode.
     *
     * @method plane
     * @param  {Number} [width]    width of the plane.
     * @param  {Number} [height]   height of the plane.
     * @param  {Integer} [detailX] number of triangle subdivisions along the x-axis.
     * @param {Integer} [detailY]  number of triangle subdivisions along the y-axis.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white plane on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the plane.
     *   plane();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white plane on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the plane.
     *   // Set its width and height to 30.
     *   plane(30);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white plane on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the plane.
     *   // Set its width to 30 and height to 50.
     *   plane(30, 50);
     * }
     */
    fn.plane = function(
      width = 50,
      height = width,
      detailX = 1,
      detailY = 1
    ) {
      this._assert3d('plane');
      // p5._validateParameters('plane', arguments);

      this._renderer.plane(width, height, detailX, detailY);
      return this;
    };

    /**
     * Draws a box (rectangular prism).
     *
     * A box is a 3D shape with six faces. Each face makes a 90˚ with four
     * neighboring faces.
     *
     * The first parameter, `width`, is optional. If a `Number` is passed, as in
     * `box(20)`, it sets the box’s width and height. By default, `width` is 50.
     *
     * The second parameter, `height`, is also optional. If a `Number` is passed,
     * as in `box(20, 30)`, it sets the box’s height. By default, `height` is set
     * to the box’s `width`.
     *
     * The third parameter, `depth`, is also optional. If a `Number` is passed, as
     * in `box(20, 30, 40)`, it sets the box’s depth. By default, `depth` is set
     * to the box’s `height`.
     *
     * The fourth parameter, `detailX`, is also optional. If a `Number` is passed,
     * as in `box(20, 30, 40, 5)`, it sets the number of triangle subdivisions to
     * use along the x-axis. All 3D shapes are made by connecting triangles to
     * form their surfaces. By default, `detailX` is 1.
     *
     * The fifth parameter, `detailY`, is also optional. If a number is passed, as
     * in `box(20, 30, 40, 5, 7)`, it sets the number of triangle subdivisions to
     * use along the y-axis. All 3D shapes are made by connecting triangles to
     * form their surfaces. By default, `detailY` is 1.
     *
     * Note: `box()` can only be used in WebGL mode.
     *
     * @method  box
     * @param  {Number} [width]     width of the box.
     * @param  {Number} [height]    height of the box.
     * @param  {Number} [depth]     depth of the box.
     * @param {Integer} [detailX]   number of triangle subdivisions along the x-axis.
     * @param {Integer} [detailY]   number of triangle subdivisions along the y-axis.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white box on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the box.
     *   box();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white box on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the box.
     *   // Set its width and height to 30.
     *   box(30);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white box on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the box.
     *   // Set its width to 30 and height to 50.
     *   box(30, 50);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white box on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the box.
     *   // Set its width to 30, height to 50, and depth to 10.
     *   box(30, 50, 10);
     * }
     */
    fn.box = function(width, height, depth, detailX, detailY) {
      this._assert3d('box');
      // p5._validateParameters('box', arguments);

      this._renderer.box(width, height, depth, detailX, detailY);

      return this;
    };

    /**
     * Draws a sphere.
     *
     * A sphere is a 3D shape with triangular faces that connect to form a round
     * surface. Spheres with few faces look like crystals. Spheres with many faces
     * have smooth surfaces and look like balls.
     *
     * The first parameter, `radius`, is optional. If a `Number` is passed, as in
     * `sphere(20)`, it sets the radius of the sphere. By default, `radius` is 50.
     *
     * The second parameter, `detailX`, is also optional. If a `Number` is passed,
     * as in `sphere(20, 5)`, it sets the number of triangle subdivisions to use
     * along the x-axis. All 3D shapes are made by connecting triangles to form
     * their surfaces. By default, `detailX` is 24.
     *
     * The third parameter, `detailY`, is also optional. If a `Number` is passed,
     * as in `sphere(20, 5, 2)`, it sets the number of triangle subdivisions to
     * use along the y-axis. All 3D shapes are made by connecting triangles to
     * form their surfaces. By default, `detailY` is 16.
     *
     * Note: `sphere()` can only be used in WebGL mode.
     *
     * @method sphere
     * @param  {Number} [radius]   radius of the sphere. Defaults to 50.
     * @param  {Integer} [detailX] number of triangle subdivisions along the x-axis. Defaults to 24.
     * @param  {Integer} [detailY] number of triangle subdivisions along the y-axis. Defaults to 16.
     *
     * @chainable
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white sphere on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the sphere.
     *   sphere();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white sphere on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the sphere.
     *   // Set its radius to 30.
     *   sphere(30);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white sphere on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the sphere.
     *   // Set its radius to 30.
     *   // Set its detailX to 6.
     *   sphere(30, 6);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white sphere on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the sphere.
     *   // Set its radius to 30.
     *   // Set its detailX to 24.
     *   // Set its detailY to 4.
     *   sphere(30, 24, 4);
     * }
     */
    fn.sphere = function(radius = 50, detailX = 24, detailY = 16) {
      this._assert3d('sphere');
      // p5._validateParameters('sphere', arguments);

      this._renderer.sphere(radius, detailX, detailY);

      return this;
    };

    /**
     * Draws a cylinder.
     *
     * A cylinder is a 3D shape with triangular faces that connect a flat bottom
     * to a flat top. Cylinders with few faces look like boxes. Cylinders with
     * many faces have smooth surfaces.
     *
     * The first parameter, `radius`, is optional. If a `Number` is passed, as in
     * `cylinder(20)`, it sets the radius of the cylinder’s base. By default,
     * `radius` is 50.
     *
     * The second parameter, `height`, is also optional. If a `Number` is passed,
     * as in `cylinder(20, 30)`, it sets the cylinder’s height. By default,
     * `height` is set to the cylinder’s `radius`.
     *
     * The third parameter, `detailX`, is also optional. If a `Number` is passed,
     * as in `cylinder(20, 30, 5)`, it sets the number of edges used to form the
     * cylinder's top and bottom. Using more edges makes the top and bottom look
     * more like circles. By default, `detailX` is 24.
     *
     * The fourth parameter, `detailY`, is also optional. If a `Number` is passed,
     * as in `cylinder(20, 30, 5, 2)`, it sets the number of triangle subdivisions
     * to use along the y-axis, between cylinder's the top and bottom. All 3D
     * shapes are made by connecting triangles to form their surfaces. By default,
     * `detailY` is 1.
     *
     * The fifth parameter, `bottomCap`, is also optional. If a `false` is passed,
     * as in `cylinder(20, 30, 5, 2, false)` the cylinder’s bottom won’t be drawn.
     * By default, `bottomCap` is `true`.
     *
     * The sixth parameter, `topCap`, is also optional. If a `false` is passed, as
     * in `cylinder(20, 30, 5, 2, false, false)` the cylinder’s top won’t be
     * drawn. By default, `topCap` is `true`.
     *
     * Note: `cylinder()` can only be used in WebGL mode.
     *
     * @method cylinder
     * @param  {Number}  [radius]    radius of the cylinder. Defaults to 50.
     * @param  {Number}  [height]    height of the cylinder. Defaults to the value of `radius`.
     * @param  {Integer} [detailX]   number of edges along the top and bottom. Defaults to 24.
     * @param  {Integer} [detailY]   number of triangle subdivisions along the y-axis. Defaults to 1.
     * @param  {Boolean} [bottomCap] whether to draw the cylinder's bottom. Defaults to `true`.
     * @param  {Boolean} [topCap]    whether to draw the cylinder's top. Defaults to `true`.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cylinder on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the cylinder.
     *   cylinder();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cylinder on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the cylinder.
     *   // Set its radius and height to 30.
     *   cylinder(30);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cylinder on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the cylinder.
     *   // Set its radius to 30 and height to 50.
     *   cylinder(30, 50);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white box on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the cylinder.
     *   // Set its radius to 30 and height to 50.
     *   // Set its detailX to 5.
     *   cylinder(30, 50, 5);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cylinder on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the cylinder.
     *   // Set its radius to 30 and height to 50.
     *   // Set its detailX to 24 and detailY to 2.
     *   cylinder(30, 50, 24, 2);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cylinder on a gray background. Its top is missing.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the cylinder.
     *   // Set its radius to 30 and height to 50.
     *   // Set its detailX to 24 and detailY to 1.
     *   // Don't draw its bottom.
     *   cylinder(30, 50, 24, 1, false);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cylinder on a gray background. Its top and bottom are missing.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the cylinder.
     *   // Set its radius to 30 and height to 50.
     *   // Set its detailX to 24 and detailY to 1.
     *   // Don't draw its bottom or top.
     *   cylinder(30, 50, 24, 1, false, false);
     * }
     */
    fn.cylinder = function(
      radius = 50,
      height = radius,
      detailX = 24,
      detailY = 1,
      bottomCap = true,
      topCap = true
    ) {
      this._assert3d('cylinder');
      // p5._validateParameters('cylinder', arguments);

      this._renderer.cylinder(
        radius,
        height,
        detailX, detailY,
        bottomCap, topCap
      );

      return this;
    };

    /**
     * Draws a cone.
     *
     * A cone is a 3D shape with triangular faces that connect a flat bottom to a
     * single point. Cones with few faces look like pyramids. Cones with many
     * faces have smooth surfaces.
     *
     * The first parameter, `radius`, is optional. If a `Number` is passed, as in
     * `cone(20)`, it sets the radius of the cone’s base. By default, `radius` is
     * 50.
     *
     * The second parameter, `height`, is also optional. If a `Number` is passed,
     * as in `cone(20, 30)`, it sets the cone’s height. By default, `height` is
     * set to the cone’s `radius`.
     *
     * The third parameter, `detailX`, is also optional. If a `Number` is passed,
     * as in `cone(20, 30, 5)`, it sets the number of edges used to form the
     * cone's base. Using more edges makes the base look more like a circle. By
     * default, `detailX` is 24.
     *
     * The fourth parameter, `detailY`, is also optional. If a `Number` is passed,
     * as in `cone(20, 30, 5, 7)`, it sets the number of triangle subdivisions to
     * use along the y-axis connecting the base to the tip. All 3D shapes are made
     * by connecting triangles to form their surfaces. By default, `detailY` is 1.
     *
     * The fifth parameter, `cap`, is also optional. If a `false` is passed, as
     * in `cone(20, 30, 5, 7, false)` the cone’s base won’t be drawn. By default,
     * `cap` is `true`.
     *
     * Note: `cone()` can only be used in WebGL mode.
     *
     * @method cone
     * @param  {Number}  [radius]  radius of the cone's base. Defaults to 50.
     * @param  {Number}  [height]  height of the cone. Defaults to the value of `radius`.
     * @param  {Integer} [detailX] number of edges used to draw the base. Defaults to 24.
     * @param  {Integer} [detailY] number of triangle subdivisions along the y-axis. Defaults to 1.
     * @param  {Boolean} [cap]     whether to draw the cone's base.  Defaults to `true`.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cone on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the cone.
     *   cone();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cone on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the cone.
     *   // Set its radius and height to 30.
     *   cone(30);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cone on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the cone.
     *   // Set its radius to 30 and height to 50.
     *   cone(30, 50);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cone on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the cone.
     *   // Set its radius to 30 and height to 50.
     *   // Set its detailX to 5.
     *   cone(30, 50, 5);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white pyramid on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the cone.
     *   // Set its radius to 30 and height to 50.
     *   // Set its detailX to 5.
     *   cone(30, 50, 5);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cone on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the cone.
     *   // Set its radius to 30 and height to 50.
     *   // Set its detailX to 24 and detailY to 2.
     *   cone(30, 50, 24, 2);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white cone on a gray background. Its base is missing.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the cone.
     *   // Set its radius to 30 and height to 50.
     *   // Set its detailX to 24 and detailY to 1.
     *   // Don't draw its base.
     *   cone(30, 50, 24, 1, false);
     * }
     */
    fn.cone = function(
      radius = 50,
      height = radius,
      detailX = 24,
      detailY = 1,
      cap = true
    ) {
      this._assert3d('cone');
      // p5._validateParameters('cone', arguments);

      this._renderer.cone(radius, height, detailX, detailY, cap);

      return this;
    };

    /**
     * Draws an ellipsoid.
     *
     * An ellipsoid is a 3D shape with triangular faces that connect to form a
     * round surface. Ellipsoids with few faces look like crystals. Ellipsoids
     * with many faces have smooth surfaces and look like eggs. `ellipsoid()`
     * defines a shape by its radii. This is different from
     * <a href="#/p5/ellipse">ellipse()</a> which uses diameters
     * (width and height).
     *
     * The first parameter, `radiusX`, is optional. If a `Number` is passed, as in
     * `ellipsoid(20)`, it sets the radius of the ellipsoid along the x-axis. By
     * default, `radiusX` is 50.
     *
     * The second parameter, `radiusY`, is also optional. If a `Number` is passed,
     * as in `ellipsoid(20, 30)`, it sets the ellipsoid’s radius along the y-axis.
     * By default, `radiusY` is set to the ellipsoid’s `radiusX`.
     *
     * The third parameter, `radiusZ`, is also optional. If a `Number` is passed,
     * as in `ellipsoid(20, 30, 40)`, it sets the ellipsoid’s radius along the
     * z-axis. By default, `radiusZ` is set to the ellipsoid’s `radiusY`.
     *
     * The fourth parameter, `detailX`, is also optional. If a `Number` is passed,
     * as in `ellipsoid(20, 30, 40, 5)`, it sets the number of triangle
     * subdivisions to use along the x-axis. All 3D shapes are made by connecting
     * triangles to form their surfaces. By default, `detailX` is 24.
     *
     * The fifth parameter, `detailY`, is also optional. If a `Number` is passed,
     * as in `ellipsoid(20, 30, 40, 5, 7)`, it sets the number of triangle
     * subdivisions to use along the y-axis. All 3D shapes are made by connecting
     * triangles to form their surfaces. By default, `detailY` is 16.
     *
     * Note: `ellipsoid()` can only be used in WebGL mode.
     *
     * @method ellipsoid
     * @param  {Number} [radiusX]  radius of the ellipsoid along the x-axis. Defaults to 50.
     * @param  {Number} [radiusY]  radius of the ellipsoid along the y-axis. Defaults to `radiusX`.
     * @param  {Number} [radiusZ]  radius of the ellipsoid along the z-axis. Defaults to `radiusY`.
     * @param  {Integer} [detailX] number of triangle subdivisions along the x-axis. Defaults to 24.
     * @param  {Integer} [detailY] number of triangle subdivisions along the y-axis. Defaults to 16.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white sphere on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the ellipsoid.
     *   // Set its radiusX to 30.
     *   ellipsoid(30);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white ellipsoid on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the ellipsoid.
     *   // Set its radiusX to 30.
     *   // Set its radiusY to 40.
     *   ellipsoid(30, 40);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white ellipsoid on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the ellipsoid.
     *   // Set its radiusX to 30.
     *   // Set its radiusY to 40.
     *   // Set its radiusZ to 50.
     *   ellipsoid(30, 40, 50);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white ellipsoid on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the ellipsoid.
     *   // Set its radiusX to 30.
     *   // Set its radiusY to 40.
     *   // Set its radiusZ to 50.
     *   // Set its detailX to 4.
     *   ellipsoid(30, 40, 50, 4);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white ellipsoid on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the ellipsoid.
     *   // Set its radiusX to 30.
     *   // Set its radiusY to 40.
     *   // Set its radiusZ to 50.
     *   // Set its detailX to 4.
     *   // Set its detailY to 3.
     *   ellipsoid(30, 40, 50, 4, 3);
     * }
     */
    fn.ellipsoid = function(
      radiusX = 50,
      radiusY = radiusX,
      radiusZ = radiusX,
      detailX = 24,
      detailY = 16
    ) {
      this._assert3d('ellipsoid');
      // p5._validateParameters('ellipsoid', arguments);

      this._renderer.ellipsoid(radiusX, radiusY, radiusZ, detailX, detailY);

      return this;
    };

    /**
     * Draws a torus.
     *
     * A torus is a 3D shape with triangular faces that connect to form a ring.
     * Toruses with few faces look flattened. Toruses with many faces have smooth
     * surfaces.
     *
     * The first parameter, `radius`, is optional. If a `Number` is passed, as in
     * `torus(30)`, it sets the radius of the ring. By default, `radius` is 50.
     *
     * The second parameter, `tubeRadius`, is also optional. If a `Number` is
     * passed, as in `torus(30, 15)`, it sets the radius of the tube. By default,
     * `tubeRadius` is 10.
     *
     * The third parameter, `detailX`, is also optional. If a `Number` is passed,
     * as in `torus(30, 15, 5)`, it sets the number of edges used to draw the hole
     * of the torus. Using more edges makes the hole look more like a circle. By
     * default, `detailX` is 24.
     *
     * The fourth parameter, `detailY`, is also optional. If a `Number` is passed,
     * as in `torus(30, 15, 5, 7)`, it sets the number of triangle subdivisions to
     * use while filling in the torus’ height. By default, `detailY` is 16.
     *
     * Note: `torus()` can only be used in WebGL mode.
     *
     * @method torus
     * @param  {Number} [radius]      radius of the torus. Defaults to 50.
     * @param  {Number} [tubeRadius]  radius of the tube. Defaults to 10.
     * @param  {Integer} [detailX]    number of edges that form the hole. Defaults to 24.
     * @param  {Integer} [detailY]    number of triangle subdivisions along the y-axis. Defaults to 16.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white torus on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the torus.
     *   torus();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white torus on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the torus.
     *   // Set its radius to 30.
     *   torus(30);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white torus on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the torus.
     *   // Set its radius to 30 and tubeRadius to 15.
     *   torus(30, 15);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white torus on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the torus.
     *   // Set its radius to 30 and tubeRadius to 15.
     *   // Set its detailX to 5.
     *   torus(30, 15, 5);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white torus on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the torus.
     *   // Set its radius to 30 and tubeRadius to 15.
     *   // Set its detailX to 5.
     *   // Set its detailY to 3.
     *   torus(30, 15, 5, 3);
     * }
     */
    fn.torus = function(radius, tubeRadius, detailX, detailY) {
      this._assert3d('torus');
      // p5._validateParameters('torus', arguments);

      this._renderer.torus(radius, tubeRadius, detailX, detailY);

      return this;
    };

    ///////////////////////
    ///  2D primitives  ///
    ///////////////////////
    //
    // Note: Documentation is not generated on the p5.js website for functions on
    // the p5.Renderer3D prototype.

    /**
     * Draws a point, a coordinate in space at the dimension of one pixel,
     * given x, y and z coordinates. The color of the point is determined
     * by the current stroke, while the point size is determined by current
     * stroke weight.
     * @private
     * @param {Number} x x-coordinate of point
     * @param {Number} y y-coordinate of point
     * @param {Number} z z-coordinate of point
     * @chainable
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     * }
     *
     * function draw() {
     *   background(50);
     *   stroke(255);
     *   strokeWeight(4);
     *   point(25, 0);
     *   strokeWeight(3);
     *   point(-25, 0);
     *   strokeWeight(2);
     *   point(0, 25);
     *   strokeWeight(1);
     *   point(0, -25);
     * }
     */
    Renderer3D.prototype.point = function(x, y, z = 0) {
      this.beginShape(POINTS);
      this.vertex(x, y, z);
      this.endShape();

      return this;
    };

    Renderer3D.prototype.triangle = function(args) {
      const x1 = args[0],
        y1 = args[1];
      const x2 = args[2],
        y2 = args[3];
      const x3 = args[4],
        y3 = args[5];

      const gid = 'tri';
      if (!this.geometryInHash(gid)) {
        const _triangle = function() {
          const vertices = [];
          vertices.push(new Vector(0, 0, 0));
          vertices.push(new Vector(1, 0, 0));
          vertices.push(new Vector(0, 1, 0));
          this.edges = [[0, 1], [1, 2], [2, 0]];
          this.vertices = vertices;
          this.faces = [[0, 1, 2]];
          this.uvs = [0, 0, 1, 0, 1, 1];
        };
        const triGeom = new Geometry(1, 1, _triangle, this);
        triGeom._edgesToVertices();
        triGeom.computeNormals();
        triGeom.gid = gid;
        this.geometryBufferCache.ensureCached(triGeom);
      }

      // only one triangle is cached, one point is at the origin, and the
      // two adjacent sides are tne unit vectors along the X & Y axes.
      //
      // this matrix multiplication transforms those two unit vectors
      // onto the required vector prior to rendering, and moves the
      // origin appropriately.
      const uModelMatrix = this.states.uModelMatrix.copy();
      try {
        // triangle orientation.
        const orientation = Math.sign(x1*y2-x2*y1 + x2*y3-x3*y2 + x3*y1-x1*y3);
        const mult = new Matrix([
          x2 - x1, y2 - y1, 0, 0, // the resulting unit X-axis
          x3 - x1, y3 - y1, 0, 0, // the resulting unit Y-axis
          0, 0, orientation, 0,   // the resulting unit Z-axis (Reflect the specified order of vertices)
          x1, y1, 0, 1            // the resulting origin
        ]).mult(this.states.uModelMatrix);

        this.states.setValue('uModelMatrix', mult);

        this.model(this.geometryBufferCache.getGeometryByID(gid));
      } finally {
        this.states.setValue('uModelMatrix', uModelMatrix);
      }

      return this;
    };

    Renderer3D.prototype.ellipse = function(args) {
      this.arc(
        args[0],
        args[1],
        args[2],
        args[3],
        0,
        TWO_PI,
        OPEN,
        args[4]
      );
    };

    Renderer3D.prototype.arc = function(...args) {
      const x = args[0];
      const y = args[1];
      const width = args[2];
      const height = args[3];
      const start = args[4];
      const stop = args[5];
      const mode = args[6];
      const detail = args[7] || 25;

      let shape;
      let gid;

      // check if it is an ellipse or an arc
      if (Math.abs(stop - start) >= TWO_PI) {
        shape = 'ellipse';
        gid = `${shape}|${detail}|`;
      } else {
        shape = 'arc';
        gid = `${shape}|${start}|${stop}|${mode}|${detail}|`;
      }

      if (!this.geometryInHash(gid)) {
        const _arc = function() {

          // if the start and stop angles are not the same, push vertices to the array
          if (start.toFixed(10) !== stop.toFixed(10)) {
            // if the mode specified is PIE or null, push the mid point of the arc in vertices
            if (mode === PIE || typeof mode === 'undefined') {
              this.vertices.push(new Vector(0.5, 0.5, 0));
              this.uvs.push([0.5, 0.5]);
            }

            // vertices for the perimeter of the circle
            for (let i = 0; i <= detail; i++) {
              const u = i / detail;
              const theta = (stop - start) * u + start;

              const _x = 0.5 + Math.cos(theta) / 2;
              const _y = 0.5 + Math.sin(theta) / 2;

              this.vertices.push(new Vector(_x, _y, 0));
              this.uvs.push([_x, _y]);

              if (i < detail - 1) {
                this.faces.push([0, i + 1, i + 2]);
                this.edges.push([i + 1, i + 2]);
              }
            }

            // check the mode specified in order to push vertices and faces, different for each mode
            switch (mode) {
              case PIE:
                this.faces.push([
                  0,
                  this.vertices.length - 2,
                  this.vertices.length - 1
                ]);
                this.edges.push([0, 1]);
                this.edges.push([
                  this.vertices.length - 2,
                  this.vertices.length - 1
                ]);
                this.edges.push([0, this.vertices.length - 1]);
                break;

              case CHORD:
                this.edges.push([0, 1]);
                this.edges.push([0, this.vertices.length - 1]);
                break;

              case OPEN:
                this.edges.push([0, 1]);
                break;

              default:
                this.faces.push([
                  0,
                  this.vertices.length - 2,
                  this.vertices.length - 1
                ]);
                this.edges.push([
                  this.vertices.length - 2,
                  this.vertices.length - 1
                ]);
            }
          }
        };

        const arcGeom = new Geometry(detail, 1, _arc, this);
        arcGeom.computeNormals();

        if (detail <= 50) {
          arcGeom._edgesToVertices(arcGeom);
        } else if (this.states.strokeColor) {
          console.log(
            `Cannot apply a stroke to an ${shape} with more than 50 detail`
          );
        }

        arcGeom.gid = gid;
        this.geometryBufferCache.ensureCached(arcGeom);
      }

      const uModelMatrix = this.states.uModelMatrix;
      this.states.setValue('uModelMatrix', this.states.uModelMatrix.clone());

      try {
        this.states.uModelMatrix.translate([x, y, 0]);
        this.states.uModelMatrix.scale(width, height, 1);

        this.model(this.geometryBufferCache.getGeometryByID(gid));
      } finally {
        this.states.setValue('uModelMatrix', uModelMatrix);
      }

      return this;
    };

    Renderer3D.prototype.rect = function(args) {
      const x = args[0];
      const y = args[1];
      const width = args[2];
      const height = args[3];

      if (typeof args[4] === 'undefined') {
        // Use the retained mode for drawing rectangle,
        // if args for rounding rectangle is not provided by user.
        const perPixelLighting = this._pInst._glAttributes?.perPixelLighting ?? true;
        const detailX = args[4] || (perPixelLighting ? 1 : 24);
        const detailY = args[5] || (perPixelLighting ? 1 : 16);
        const gid = `rect|${detailX}|${detailY}`;
        if (!this.geometryInHash(gid)) {
          const _rect = function() {
            for (let i = 0; i <= this.detailY; i++) {
              const v = i / this.detailY;
              for (let j = 0; j <= this.detailX; j++) {
                const u = j / this.detailX;
                const p = new Vector(u, v, 0);
                this.vertices.push(p);
                this.uvs.push(u, v);
              }
            }
            // using stroke indices to avoid stroke over face(s) of rectangle
            if (detailX > 0 && detailY > 0) {
              this.edges = [
                [0, detailX],
                [detailX, (detailX + 1) * (detailY + 1) - 1],
                [(detailX + 1) * (detailY + 1) - 1, (detailX + 1) * detailY],
                [(detailX + 1) * detailY, 0]
              ];
            }
          };
          const rectGeom = new Geometry(detailX, detailY, _rect, this);
          rectGeom
            .computeFaces()
            .computeNormals()
            ._edgesToVertices();
          rectGeom.gid = gid;
          this.geometryBufferCache.ensureCached(rectGeom);
        }

        // only a single rectangle (of a given detail) is cached: a square with
        // opposite corners at (0,0) & (1,1).
        //
        // before rendering, this square is scaled & moved to the required location.
        const uModelMatrix = this.states.uModelMatrix;
        this.states.setValue('uModelMatrix', this.states.uModelMatrix.copy());
        try {
          this.states.uModelMatrix.translate([x, y, 0]);
          this.states.uModelMatrix.scale(width, height, 1);

          this.model(this.geometryBufferCache.getGeometryByID(gid));
        } finally {
          this.states.setValue('uModelMatrix', uModelMatrix);
        }
      } else {
        // Use Immediate mode to round the rectangle corner,
        // if args for rounding corners is provided by user
        let tl = args[4];
        let tr = typeof args[5] === 'undefined' ? tl : args[5];
        let br = typeof args[6] === 'undefined' ? tr : args[6];
        let bl = typeof args[7] === 'undefined' ? br : args[7];

        let a = x;
        let b = y;
        let c = width;
        let d = height;

        c += a;
        d += b;

        if (a > c) {
          const temp = a;
          a = c;
          c = temp;
        }

        if (b > d) {
          const temp = b;
          b = d;
          d = temp;
        }

        const maxRounding = Math.min((c - a) / 2, (d - b) / 2);
        if (tl > maxRounding) tl = maxRounding;
        if (tr > maxRounding) tr = maxRounding;
        if (br > maxRounding) br = maxRounding;
        if (bl > maxRounding) bl = maxRounding;

        let x1 = a;
        let y1 = b;
        let x2 = c;
        let y2 = d;

        const prevMode = this.states.textureMode;
        this.states.setValue('textureMode', NORMAL);
        const prevOrder = this.bezierOrder();
        this.bezierOrder(2);
        this.beginShape();
        const addUVs = (x, y) => [x, y, 0, (x - x1)/width, (y - y1)/height];
        if (tr !== 0) {
          this.vertex(...addUVs(x2 - tr, y1));
          this.bezierVertex(...addUVs(x2, y1));
          this.bezierVertex(...addUVs(x2, y1 + tr));
        } else {
          this.vertex(...addUVs(x2, y1));
        }
        if (br !== 0) {
          this.vertex(...addUVs(x2, y2 - br));
          this.bezierVertex(...addUVs(x2, y2));
          this.bezierVertex(...addUVs(x2 - br, y2));
        } else {
          this.vertex(...addUVs(x2, y2));
        }
        if (bl !== 0) {
          this.vertex(...addUVs(x1 + bl, y2));
          this.bezierVertex(...addUVs(x1, y2));
          this.bezierVertex(...addUVs(x1, y2 - bl));
        } else {
          this.vertex(...addUVs(x1, y2));
        }
        if (tl !== 0) {
          this.vertex(...addUVs(x1, y1 + tl));
          this.bezierVertex(...addUVs(x1, y1));
          this.bezierVertex(...addUVs(x1 + tl, y1));
        } else {
          this.vertex(...addUVs(x1, y1));
        }

        this.endShape(CLOSE);
        this.states.setValue('textureMode', prevMode);
        this.bezierOrder(prevOrder);
      }
      return this;
    };

    Renderer3D.prototype.quad = function(
      x1, y1, z1,
      x2, y2, z2,
      x3, y3, z3,
      x4, y4, z4,
      detailX=2,
      detailY=2
    ) {

      const gid =
        `quad|${x1}|${y1}|${z1}|${x2}|${y2}|${z2}|${x3}|${y3}|${z3}|${x4}|${y4}|${z4}|${detailX}|${detailY}`;

      if (!this.geometryInHash(gid)) {
        const quadGeom = new Geometry(detailX, detailY, function() {
          //algorithm adapted from c++ to js
          //https://stackoverflow.com/questions/16989181/whats-the-correct-way-to-draw-a-distorted-plane-in-opengl/16993202#16993202
          let xRes = 1.0 / (this.detailX - 1);
          let yRes = 1.0 / (this.detailY - 1);
          for (let y = 0; y < this.detailY; y++) {
            for (let x = 0; x < this.detailX; x++) {
              let pctx = x * xRes;
              let pcty = y * yRes;

              let linePt0x = (1 - pcty) * x1 + pcty * x4;
              let linePt0y = (1 - pcty) * y1 + pcty * y4;
              let linePt0z = (1 - pcty) * z1 + pcty * z4;
              let linePt1x = (1 - pcty) * x2 + pcty * x3;
              let linePt1y = (1 - pcty) * y2 + pcty * y3;
              let linePt1z = (1 - pcty) * z2 + pcty * z3;

              let ptx = (1 - pctx) * linePt0x + pctx * linePt1x;
              let pty = (1 - pctx) * linePt0y + pctx * linePt1y;
              let ptz = (1 - pctx) * linePt0z + pctx * linePt1z;

              this.vertices.push(new Vector(ptx, pty, ptz));
              this.uvs.push([pctx, pcty]);
            }
          }
        }, this);

        quadGeom.faces = [];
        for(let y = 0; y < detailY-1; y++){
          for(let x = 0; x < detailX-1; x++){
            let pt0 = x + y * detailX;
            let pt1 = (x + 1) + y * detailX;
            let pt2 = (x + 1) + (y + 1) * detailX;
            let pt3 = x + (y + 1) * detailX;
            quadGeom.faces.push([pt0, pt1, pt2]);
            quadGeom.faces.push([pt0, pt2, pt3]);
          }
        }
        quadGeom.computeNormals();
        quadGeom.edges.length = 0;
        const vertexOrder = [0, 2, 3, 1];
        for (let i = 0; i < vertexOrder.length; i++) {
          const startVertex = vertexOrder[i];
          const endVertex = vertexOrder[(i + 1) % vertexOrder.length];
          quadGeom.edges.push([startVertex, endVertex]);
        }
        quadGeom._edgesToVertices();
        quadGeom.gid = gid;
        this.geometryBufferCache.ensureCached(quadGeom);
      }
      this.model(this.geometryBufferCache.getGeometryByID(gid));
      return this;
    };

    //this implementation of bezier curve
    //is based on Bernstein polynomial
    // pretier-ignore
    Renderer3D.prototype.bezier = function(
      x1,
      y1,
      z1, // x2
      x2, // y2
      y2, // x3
      z2, // y3
      x3, // x4
      y3, // y4
      z3,
      x4,
      y4,
      z4
    ) {
      if (arguments.length === 8) {
        y4 = y3;
        x4 = x3;
        y3 = z2;
        x3 = y2;
        y2 = x2;
        x2 = z1;
        z1 = z2 = z3 = z4 = 0;
      }
      // TODO: handle quadratic?
      this.bezierOrder();
      this.bezierOrder(3);
      this.beginShape();
      this.vertex(x1, y1, z1);
      this.bezierVertex(x2, y2, z2);
      this.bezierVertex(x3, y3, z3);
      this.bezierVertex(x4, y4, z4);
      this.endShape();
    };

    // pretier-ignore
    Renderer3D.prototype.curve = function(
      x1,
      y1,
      z1, // x2
      x2, // y2
      y2, // x3
      z2, // y3
      x3, // x4
      y3, // y4
      z3,
      x4,
      y4,
      z4
    ) {
      if (arguments.length === 8) {
        x4 = x3;
        y4 = y3;
        x3 = y2;
        y3 = x2;
        x2 = z1;
        y2 = x2;
        z1 = z2 = z3 = z4 = 0;
      }
      this.beginShape();
      this.splineVertex(x1, y1, z1);
      this.splineVertex(x2, y2, z2);
      this.splineVertex(x3, y3, z3);
      this.splineVertex(x4, y4, z4);
      this.endShape();
    };

    /**
     * Draw a line given two points
     * @private
     * @param {Number} x0 x-coordinate of first vertex
     * @param {Number} y0 y-coordinate of first vertex
     * @param {Number} z0 z-coordinate of first vertex
     * @param {Number} x1 x-coordinate of second vertex
     * @param {Number} y1 y-coordinate of second vertex
     * @param {Number} z1 z-coordinate of second vertex
     * @chainable
     * @example
     * //draw a line
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     * }
     *
     * function draw() {
     *   background(200);
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.01);
     *   // Use fill instead of stroke to change the color of shape.
     *   fill(255, 0, 0);
     *   line(10, 10, 0, 60, 60, 20);
     * }
     */
    Renderer3D.prototype.line = function(...args) {
      if (args.length === 6) {
        // TODO shapes refactor
        this.beginShape(LINES);
        this.vertex(args[0], args[1], args[2]);
        this.vertex(args[3], args[4], args[5]);
        this.endShape();
      } else if (args.length === 4) {
        this.beginShape(LINES);
        this.vertex(args[0], args[1], 0);
        this.vertex(args[2], args[3], 0);
        this.endShape();
      }
      return this;
    };

    Renderer3D.prototype.image = function(
      img,
      sx,
      sy,
      sWidth,
      sHeight,
      dx,
      dy,
      dWidth,
      dHeight
    ) {
      // console.log(arguments);
      if (this._isErasing) {
        this.blendMode(this._cachedBlendMode);
      }

      this.push();
      this.noLights();
      this.states.setValue('strokeColor', null);

      this.texture(img);
      this.states.setValue('textureMode', NORMAL);

      let u0 = 0;
      if (sx <= img.width) {
        u0 = sx / img.width;
      }

      let u1 = 1;
      if (sx + sWidth <= img.width) {
        u1 = (sx + sWidth) / img.width;
      }

      let v0 = 0;
      if (sy <= img.height) {
        v0 = sy / img.height;
      }

      let v1 = 1;
      if (sy + sHeight <= img.height) {
        v1 = (sy + sHeight) / img.height;
      }

      this._drawingImage = true;
      this.beginShape();
      this.vertex(dx, dy, 0, u0, v0);
      this.vertex(dx + dWidth, dy, 0, u1, v0);
      this.vertex(dx + dWidth, dy + dHeight, 0, u1, v1);
      this.vertex(dx, dy + dHeight, 0, u0, v1);
      this.endShape(CLOSE);
      this._drawingImage = false;

      this.pop();

      if (this._isErasing) {
        this.blendMode(REMOVE);
      }
    };

    ///////////////////////
    ///  3D primitives  ///
    ///////////////////////
    /**
     * @private
     * Helper function for creating both cones and cylinders
     * Will only generate well-defined geometry when bottomRadius, height > 0
     * and topRadius >= 0
     * If topRadius == 0, topCap should be false
     */
    const _truncatedCone = function(
      bottomRadius,
      topRadius,
      height,
      detailX,
      detailY,
      bottomCap,
      topCap
    ) {
      bottomRadius = bottomRadius <= 0 ? 1 : bottomRadius;
      topRadius = topRadius < 0 ? 0 : topRadius;
      height = height <= 0 ? bottomRadius : height;
      detailX = detailX < 3 ? 3 : detailX;
      detailY = detailY < 1 ? 1 : detailY;
      bottomCap = bottomCap === undefined ? true : bottomCap;
      topCap = topCap === undefined ? topRadius !== 0 : topCap;
      const start = bottomCap ? -2 : 0;
      const end = detailY + (topCap ? 2 : 0);
      //ensure constant slant for interior vertex normals
      const slant = Math.atan2(bottomRadius - topRadius, height);
      const sinSlant = Math.sin(slant);
      const cosSlant = Math.cos(slant);
      let yy, ii, jj;
      for (yy = start; yy <= end; ++yy) {
        let v = yy / detailY;
        let y = height * v;
        let ringRadius;
        if (yy < 0) {
          //for the bottomCap edge
          y = 0;
          v = 0;
          ringRadius = bottomRadius;
        } else if (yy > detailY) {
          //for the topCap edge
          y = height;
          v = 1;
          ringRadius = topRadius;
        } else {
          //for the middle
          ringRadius = bottomRadius + (topRadius - bottomRadius) * v;
        }
        if (yy === -2 || yy === detailY + 2) {
          //center of bottom or top caps
          ringRadius = 0;
        }

        y -= height / 2; //shift coordiate origin to the center of object
        for (ii = 0; ii < detailX; ++ii) {
          const u = ii / (detailX - 1);
          const ur = 2 * Math.PI * u;
          const sur = Math.sin(ur);
          const cur = Math.cos(ur);

          //VERTICES
          this.vertices.push(new Vector(sur * ringRadius, y, cur * ringRadius));

          //VERTEX NORMALS
          let vertexNormal;
          if (yy < 0) {
            vertexNormal = new Vector(0, -1, 0);
          } else if (yy > detailY && topRadius) {
            vertexNormal = new Vector(0, 1, 0);
          } else {
            vertexNormal = new Vector(sur * cosSlant, sinSlant, cur * cosSlant);
          }
          this.vertexNormals.push(vertexNormal);
          //UVs
          this.uvs.push(u, v);
        }
      }

      let startIndex = 0;
      if (bottomCap) {
        for (jj = 0; jj < detailX; ++jj) {
          const nextjj = (jj + 1) % detailX;
          this.faces.push([
            startIndex + jj,
            startIndex + detailX + nextjj,
            startIndex + detailX + jj
          ]);
        }
        startIndex += detailX * 2;
      }
      for (yy = 0; yy < detailY; ++yy) {
        for (ii = 0; ii < detailX; ++ii) {
          const nextii = (ii + 1) % detailX;
          this.faces.push([
            startIndex + ii,
            startIndex + nextii,
            startIndex + detailX + nextii
          ]);
          this.faces.push([
            startIndex + ii,
            startIndex + detailX + nextii,
            startIndex + detailX + ii
          ]);
        }
        startIndex += detailX;
      }
      if (topCap) {
        startIndex += detailX;
        for (ii = 0; ii < detailX; ++ii) {
          this.faces.push([
            startIndex + ii,
            startIndex + (ii + 1) % detailX,
            startIndex + detailX
          ]);
        }
      }
    };

    Renderer3D.prototype.plane = function(
      width = 50,
      height = width,
      detailX = 1,
      detailY = 1
    ) {
      const gid = `plane|${detailX}|${detailY}`;

      if (!this.geometryInHash(gid)) {
        const _plane = function() {
          let u, v, p;
          for (let i = 0; i <= this.detailY; i++) {
            v = i / this.detailY;
            for (let j = 0; j <= this.detailX; j++) {
              u = j / this.detailX;
              p = new Vector(u - 0.5, v - 0.5, 0);
              this.vertices.push(p);
              this.uvs.push(u, v);
            }
          }
        };
        const planeGeom = new Geometry(detailX, detailY, _plane, this);
        planeGeom.computeFaces().computeNormals();
        if (detailX <= 1 && detailY <= 1) {
          planeGeom._makeTriangleEdges()._edgesToVertices();
        } else if (this.states.strokeColor) {
          console.log(
            'Cannot draw stroke on plane objects with more' +
            ' than 1 detailX or 1 detailY'
          );
        }
        planeGeom.gid = gid;
        this.geometryBufferCache.ensureCached(planeGeom);
      }

      this._drawGeometryScaled(
        this.geometryBufferCache.getGeometryByID(gid),
        width, height,
        1
      );
    };

    Renderer3D.prototype.box = function(
      width = 50,
      height = width,
      depth = height,
      detailX,
      detailY
    ){
      const perPixelLighting =
        this.attributes && this.attributes.perPixelLighting;
      if (typeof detailX === 'undefined') {
        detailX = perPixelLighting ? 1 : 4;
      }
      if (typeof detailY === 'undefined') {
        detailY = perPixelLighting ? 1 : 4;
      }

      const gid = `box|${detailX}|${detailY}`;
      if (!this.geometryInHash(gid)) {
        const _box = function() {
          const cubeIndices = [
            [0, 4, 2, 6], // -1, 0, 0],// -x
            [1, 3, 5, 7], // +1, 0, 0],// +x
            [0, 1, 4, 5], // 0, -1, 0],// -y
            [2, 6, 3, 7], // 0, +1, 0],// +y
            [0, 2, 1, 3], // 0, 0, -1],// -z
            [4, 5, 6, 7] // 0, 0, +1] // +z
          ];
          //using custom edges
          //to avoid diagonal stroke lines across face of box
          this.edges = [
            [0, 1],
            [1, 3],
            [3, 2],
            [6, 7],
            [8, 9],
            [9, 11],
            [14, 15],
            [16, 17],
            [17, 19],
            [18, 19],
            [20, 21],
            [22, 23]
          ];

          cubeIndices.forEach((cubeIndex, i) => {
            const v = i * 4;
            for (let j = 0; j < 4; j++) {
              const d = cubeIndex[j];
              //inspired by lightgl:
              //https://github.com/evanw/lightgl.js
              //octants:https://en.wikipedia.org/wiki/Octant_(solid_geometry)
              const octant = new Vector(
                ((d & 1) * 2 - 1) / 2,
                ((d & 2) - 1) / 2,
                ((d & 4) / 2 - 1) / 2
              );
              this.vertices.push(octant);
              this.uvs.push(j & 1, (j & 2) / 2);
            }
            this.faces.push([v, v + 1, v + 2]);
            this.faces.push([v + 2, v + 1, v + 3]);
          });
        };
        const boxGeom = new Geometry(detailX, detailY, _box, this);
        boxGeom.computeNormals();
        if (detailX <= 4 && detailY <= 4) {
          boxGeom._edgesToVertices();
        } else if (this.states.strokeColor) {
          console.log(
            'Cannot draw stroke on box objects with more' +
            ' than 4 detailX or 4 detailY'
          );
        }
        //initialize our geometry buffer with
        //the key val pair:
        //geometry Id, Geom object
        boxGeom.gid = gid;
        this.geometryBufferCache.ensureCached(boxGeom);
      }
      this._drawGeometryScaled(
        this.geometryBufferCache.getGeometryByID(gid),
        width, height,
        depth
      );
    };

    Renderer3D.prototype.sphere = function(
      radius = 50,
      detailX = 24,
      detailY = 16
    ) {
      this.ellipsoid(radius, radius, radius, detailX, detailY);
    };

    Renderer3D.prototype.ellipsoid = function(
      radiusX = 50,
      radiusY = radiusX,
      radiusZ = radiusX,
      detailX = 24,
      detailY = 16
    ) {
      const gid = `ellipsoid|${detailX}|${detailY}`;

      if (!this.geometryInHash(gid)) {
        const _ellipsoid = function() {
          for (let i = 0; i <= this.detailY; i++) {
            const v = i / this.detailY;
            const phi = Math.PI * v - Math.PI / 2;
            const cosPhi = Math.cos(phi);
            const sinPhi = Math.sin(phi);

            for (let j = 0; j <= this.detailX; j++) {
              const u = j / this.detailX;
              const theta = 2 * Math.PI * u;
              const cosTheta = Math.cos(theta);
              const sinTheta = Math.sin(theta);
              const p = new p5.Vector(
                cosPhi * sinTheta,
                sinPhi,
                cosPhi * cosTheta
              );
              this.vertices.push(p);
              this.vertexNormals.push(p);
              this.uvs.push(u, v);
            }
          }
        };
        const ellipsoidGeom = new Geometry(detailX, detailY, _ellipsoid, this);
        ellipsoidGeom.computeFaces();
        if (detailX <= 24 && detailY <= 24) {
          ellipsoidGeom._makeTriangleEdges()._edgesToVertices();
        } else if (this.states.strokeColor) {
          console.log(
            'Cannot draw stroke on ellipsoids with more' +
            ' than 24 detailX or 24 detailY'
          );
        }
        ellipsoidGeom.gid = gid;
        this.geometryBufferCache.ensureCached(ellipsoidGeom);
      }

      this._drawGeometryScaled(
        this.geometryBufferCache.getGeometryByID(gid),
        radiusX, radiusY, radiusZ
      );
    };

    Renderer3D.prototype.cylinder = function(
      radius = 50,
      height = radius,
      detailX = 24,
      detailY = 1,
      bottomCap = true,
      topCap = true
    ) {
      const gid = `cylinder|${detailX}|${detailY}|${bottomCap}|${topCap}`;
      if (!this.geometryInHash(gid)) {
        const cylinderGeom = new p5.Geometry(detailX, detailY, function() {
          _truncatedCone.call(
            this,
            1,
            1,
            1,
            detailX,
            detailY,
            bottomCap,
            topCap
          );
        }, this);
        // normals are computed in call to _truncatedCone
        if (detailX <= 24 && detailY <= 16) {
          cylinderGeom._makeTriangleEdges()._edgesToVertices();
        } else if (this.states.strokeColor) {
          console.log(
            'Cannot draw stroke on cylinder objects with more' +
            ' than 24 detailX or 16 detailY'
          );
        }
        cylinderGeom.gid = gid;
        this.geometryBufferCache.ensureCached(cylinderGeom);
      }

      this._drawGeometryScaled(
        this.geometryBufferCache.getGeometryByID(gid),
        radius,
        height,
        radius
      );
    };

    Renderer3D.prototype.cone = function(
      radius = 50,
      height = radius,
      detailX = 24,
      detailY = 1,
      cap = true
    ) {
      const gid = `cone|${detailX}|${detailY}|${cap}`;
      if (!this.geometryInHash(gid)) {
        const coneGeom = new Geometry(detailX, detailY, function() {
          _truncatedCone.call(
            this,
            1,
            0,
            1,
            detailX,
            detailY,
            cap,
            false
          );
        }, this);
        if (detailX <= 24 && detailY <= 16) {
          coneGeom._makeTriangleEdges()._edgesToVertices();
        } else if (this.states.strokeColor) {
          console.log(
            'Cannot draw stroke on cone objects with more' +
            ' than 24 detailX or 16 detailY'
          );
        }
        coneGeom.gid = gid;
        this.geometryBufferCache.ensureCached(coneGeom);
      }

      this._drawGeometryScaled(
        this.geometryBufferCache.getGeometryByID(gid),
        radius,
        height,
        radius
      );
    };

    Renderer3D.prototype.torus = function(
      radius = 50,
      tubeRadius = 10,
      detailX = 24,
      detailY = 16
    ) {
      if (radius === 0) {
        return; // nothing to draw
      }

      if (tubeRadius === 0) {
        return; // nothing to draw
      }

      const tubeRatio = (tubeRadius / radius).toPrecision(4);
      const gid = `torus|${tubeRatio}|${detailX}|${detailY}`;

      if (!this.geometryInHash(gid)) {
        const _torus = function() {
          for (let i = 0; i <= this.detailY; i++) {
            const v = i / this.detailY;
            const phi = 2 * Math.PI * v;
            const cosPhi = Math.cos(phi);
            const sinPhi = Math.sin(phi);
            const r = 1 + tubeRatio * cosPhi;

            for (let j = 0; j <= this.detailX; j++) {
              const u = j / this.detailX;
              const theta = 2 * Math.PI * u;
              const cosTheta = Math.cos(theta);
              const sinTheta = Math.sin(theta);

              const p = new Vector(
                r * cosTheta,
                r * sinTheta,
                tubeRatio * sinPhi
              );

              const n = new Vector(cosPhi * cosTheta, cosPhi * sinTheta, sinPhi);

              this.vertices.push(p);
              this.vertexNormals.push(n);
              this.uvs.push(u, v);
            }
          }
        };
        const torusGeom = new Geometry(detailX, detailY, _torus, this);
        torusGeom.computeFaces();
        if (detailX <= 24 && detailY <= 16) {
          torusGeom._makeTriangleEdges()._edgesToVertices();
        } else if (this.states.strokeColor) {
          console.log(
            'Cannot draw strokes on torus object with more' +
            ' than 24 detailX or 16 detailY'
          );
        }
        torusGeom.gid = gid;
        this.geometryBufferCache.ensureCached(torusGeom);
      }
      this._drawGeometryScaled(
        this.geometryBufferCache.getGeometryByID(gid),
        radius,
        radius,
        radius
      );
    };

    /**
     * Sets the number of segments used to draw spline curves in WebGL mode.
     *
     * In WebGL mode, smooth shapes are drawn using many flat segments. Adding
     * more flat segments makes shapes appear smoother.
     *
     * The parameter, `detail`, is the density of segments to use while drawing a
     * spline curve.
     *
     * Note: `curveDetail()` has no effect in 2D mode.
     *
     * @method curveDetail
     * @param {Number} resolution number of segments to use. Default is 1/4
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   // Set the curveDetail() to 0.5
     *   curveDetail(0.5);
     *
     *   // Do not show all the vertices
     *   splineProperty('ends', EXCLUDE)
     *
     *   // Draw a black spline curve.
     *   noFill();
     *   strokeWeight(1);
     *   stroke(0);
     *   spline(-45, -24, 0, 23, -26, 0, 23, 11, 0, -35, 15, 0);
     *
     *   // Draw red spline curves from the anchor points to the control points.
     *   spline(255, 0, 0);
     *   spline(-45, -24, 0, -45, -24, 0, 23, -26, 0, 23, 11, 0);
     *   spline(23, -26, 0, 23, 11, 0, -35, 15, 0, -35, 15, 0);
     *
     *   // Draw the anchor points in black.
     *   strokeWeight(5);
     *   stroke(0);
     *   point(23, -26);
     *   point(23, 11);
     *
     *   // Draw the control points in red.
     *   stroke(255, 0, 0);
     *   point(-45, -24);
     *   point(-35, 15);
     *
     *   describe(
     *     'A gray square with a jagged curve drawn in three segments. The curve is a sideways U shape with red segments on top and bottom, and a black segment on the right. The endpoints of all the segments are marked with dots.'
     *   );
     * }
     */
    fn.curveDetail = function(d) {
      if (!(this._renderer instanceof Renderer3D)) {
        throw new Error(
          'curveDetail() only works in WebGL mode. Did you mean to call createCanvas(width, height, WEBGL)?'
        );
      }
      return this._renderer.curveDetail(d);
    };
  }

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

  /**
   * @module 3D
   * @submodule Lights
   * @for p5
   * @requires core
   */


  function light(p5, fn){
    /**
     * Creates a light that shines from all directions.
     *
     * Ambient light does not come from one direction. Instead, 3D shapes are
     * lit evenly from all sides. Ambient lights are almost always used in
     * combination with other types of lights.
     *
     * There are three ways to call `ambientLight()` with optional parameters to
     * set the light’s color.
     *
     * The first way to call `ambientLight()` has two parameters, `gray` and
     * `alpha`. `alpha` is optional. Grayscale and alpha values between 0 and 255
     * can be passed to set the ambient light’s color, as in `ambientLight(50)` or
     * `ambientLight(50, 30)`.
     *
     * The second way to call `ambientLight()` has one parameter, color. A
     * <a href="#/p5.Color">p5.Color</a> object, an array of color values, or a
     * CSS color string, as in `ambientLight('magenta')`, can be passed to set the
     * ambient light’s color.
     *
     * The third way to call `ambientLight()` has four parameters, `v1`, `v2`,
     * `v3`, and `alpha`. `alpha` is optional. RGBA, HSBA, or HSLA values can be
     * passed to set the ambient light’s colors, as in `ambientLight(255, 0, 0)`
     * or `ambientLight(255, 0, 0, 30)`. Color values will be interpreted using
     * the current <a href="#/p5/colorMode">colorMode()</a>.
     *
     * @method ambientLight
     * @param  {Number}        v1 red or hue value in the current
     *                            <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number}        v2 green or saturation value in the current
     *                            <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number}        v3 blue, brightness, or lightness value in the current
     *                            <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number}        [alpha] alpha (transparency) value in the current
     *                                 <a href="#/p5/colorMode">colorMode()</a>.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     * // Double-click the canvas to turn on the light.
     *
     * let isLit = false;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A sphere drawn against a gray background. The sphere appears to change color when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Control the light.
     *   if (isLit === true) {
     *     // Use a grayscale value of 80.
     *     ambientLight(80);
     *   }
     *
     *   // Draw the sphere.
     *   sphere(30);
     * }
     *
     * // Turn on the ambient light when the user double-clicks.
     * function doubleClicked() {
     *   isLit = true;
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A faded magenta sphere drawn against a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   // Use a p5.Color object.
     *   let c = color('orchid');
     *   ambientLight(c);
     *
     *   // Draw the sphere.
     *   sphere();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A faded magenta sphere drawn against a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   // Use a CSS color string.
     *   ambientLight('#DA70D6');
     *
     *   // Draw the sphere.
     *   sphere(30);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A faded magenta sphere drawn against a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   // Use RGB values
     *   ambientLight(218, 112, 214);
     *
     *   // Draw the sphere.
     *   sphere(30);
     * }
     */
    /**
     * @method ambientLight
     * @param  {Number}        gray  grayscale value between 0 and 255.
     * @param  {Number}        [alpha]
     * @chainable
     */
    /**
     * @method ambientLight
     * @param  {String}        value color as a CSS string.
     * @chainable
     */
    /**
     * @method ambientLight
     * @param  {Number[]}      values color as an array of RGBA, HSBA, or HSLA
     *                                 values.
     * @chainable
     */
    /**
     * @method ambientLight
     * @param  {p5.Color}      color color as a <a href="#/p5.Color">p5.Color</a> object.
     * @chainable
     */
    fn.ambientLight = function (v1, v2, v3, a) {
      this._assert3d('ambientLight');
      // p5._validateParameters('ambientLight', arguments);

      this._renderer.ambientLight(...arguments);

      return this;
    };

    /**
     * Sets the specular color for lights.
     *
     * `specularColor()` affects lights that bounce off a surface in a preferred
     * direction. These lights include
     * <a href="#/p5/directionalLight">directionalLight()</a>,
     * <a href="#/p5/pointLight">pointLight()</a>, and
     * <a href="#/p5/spotLight">spotLight()</a>. The function helps to create
     * highlights on <a href="#/p5.Geometry">p5.Geometry</a> objects that are
     * styled with <a href="#/p5/specularMaterial">specularMaterial()</a>. If a
     * geometry does not use
     * <a href="#/p5/specularMaterial">specularMaterial()</a>, then
     * `specularColor()` will have no effect.
     *
     * Note: `specularColor()` doesn’t affect lights that bounce in all
     * directions, including <a href="#/p5/ambientLight">ambientLight()</a> and
     * <a href="#/p5/imageLight">imageLight()</a>.
     *
     * There are three ways to call `specularColor()` with optional parameters to
     * set the specular highlight color.
     *
     * The first way to call `specularColor()` has two optional parameters, `gray`
     * and `alpha`. Grayscale and alpha values between 0 and 255, as in
     * `specularColor(50)` or `specularColor(50, 80)`, can be passed to set the
     * specular highlight color.
     *
     * The second way to call `specularColor()` has one optional parameter,
     * `color`. A <a href="#/p5.Color">p5.Color</a> object, an array of color
     * values, or a CSS color string can be passed to set the specular highlight
     * color.
     *
     * The third way to call `specularColor()` has four optional parameters, `v1`,
     * `v2`, `v3`, and `alpha`. RGBA, HSBA, or HSLA values, as in
     * `specularColor(255, 0, 0, 80)`, can be passed to set the specular highlight
     * color. Color values will be interpreted using the current
     * <a href="#/p5/colorMode">colorMode()</a>.
     *
     * @method specularColor
     * @param  {Number}        v1 red or hue value in the current
     *                            <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number}        v2 green or saturation value in the current
     *                            <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number}        v3 blue, brightness, or lightness value in the current
     *                            <a href="#/p5/colorMode">colorMode()</a>.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white sphere drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // No specular color.
     *   // Draw the sphere.
     *   sphere(30);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     * // Double-click the canvas to add a point light.
     *
     * let isLit = false;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A sphere drawn on a gray background. A spotlight starts shining when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Style the sphere.
     *   noStroke();
     *   specularColor(100);
     *   specularMaterial(255, 255, 255);
     *
     *   // Control the light.
     *   if (isLit === true) {
     *     // Add a white point light from the top-right.
     *     pointLight(255, 255, 255, 30, -20, 40);
     *   }
     *
     *   // Draw the sphere.
     *   sphere(30);
     * }
     *
     * // Turn on the point light when the user double-clicks.
     * function doubleClicked() {
     *   isLit = true;
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A black sphere drawn on a gray background. An area on the surface of the sphere is highlighted in blue.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Add a specular highlight.
     *   // Use a p5.Color object.
     *   let c = color('dodgerblue');
     *   specularColor(c);
     *
     *   // Add a white point light from the top-right.
     *   pointLight(255, 255, 255, 30, -20, 40);
     *
     *   // Style the sphere.
     *   noStroke();
     *
     *   // Add a white specular material.
     *   specularMaterial(255, 255, 255);
     *
     *   // Draw the sphere.
     *   sphere(30);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A black sphere drawn on a gray background. An area on the surface of the sphere is highlighted in blue.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Add a specular highlight.
     *   // Use a CSS color string.
     *   specularColor('#1E90FF');
     *
     *   // Add a white point light from the top-right.
     *   pointLight(255, 255, 255, 30, -20, 40);
     *
     *   // Style the sphere.
     *   noStroke();
     *
     *   // Add a white specular material.
     *   specularMaterial(255, 255, 255);
     *
     *   // Draw the sphere.
     *   sphere(30);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A black sphere drawn on a gray background. An area on the surface of the sphere is highlighted in blue.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Add a specular highlight.
     *   // Use RGB values.
     *   specularColor(30, 144, 255);
     *
     *   // Add a white point light from the top-right.
     *   pointLight(255, 255, 255, 30, -20, 40);
     *
     *   // Style the sphere.
     *   noStroke();
     *
     *   // Add a white specular material.
     *   specularMaterial(255, 255, 255);
     *
     *   // Draw the sphere.
     *   sphere(30);
     * }
     */
    /**
     * @method specularColor
     * @param  {Number}        gray grayscale value between 0 and 255.
     * @chainable
     */
    /**
     * @method specularColor
     * @param  {String}        value color as a CSS string.
     * @chainable
     */
    /**
     * @method specularColor
     * @param  {Number[]}      values color as an array of RGBA, HSBA, or HSLA
     *                                 values.
     * @chainable
     */
    /**
     * @method specularColor
     * @param  {p5.Color}      color color as a <a href="#/p5.Color">p5.Color</a> object.
     * @chainable
     */
    fn.specularColor = function (v1, v2, v3) {
      this._assert3d('specularColor');
      // p5._validateParameters('specularColor', arguments);

      this._renderer.specularColor(...arguments);

      return this;
    };

    /**
     * Creates a light that shines in one direction.
     *
     * Directional lights don’t shine from a specific point. They’re like a sun
     * that shines from somewhere offscreen. The light’s direction is set using
     * three `(x, y, z)` values between -1 and 1. For example, setting a light’s
     * direction as `(1, 0, 0)` will light <a href="#/p5.Geometry">p5.Geometry</a>
     * objects from the left since the light faces directly to the right. A
     * maximum of 5 directional lights can be active at once.
     *
     * There are four ways to call `directionalLight()` with parameters to set the
     * light’s color and direction.
     *
     * The first way to call `directionalLight()` has six parameters. The first
     * three parameters, `v1`, `v2`, and `v3`, set the light’s color using the
     * current <a href="#/p5/colorMode">colorMode()</a>. The last three
     * parameters, `x`, `y`, and `z`, set the light’s direction. For example,
     * `directionalLight(255, 0, 0, 1, 0, 0)` creates a red `(255, 0, 0)` light
     * that shines to the right `(1, 0, 0)`.
     *
     * The second way to call `directionalLight()` has four parameters. The first
     * three parameters, `v1`, `v2`, and `v3`, set the light’s color using the
     * current <a href="#/p5/colorMode">colorMode()</a>. The last parameter,
     * `direction` sets the light’s direction using a
     * <a href="#/p5.Vector">p5.Vector</a> object. For example,
     * `directionalLight(255, 0, 0, lightDir)` creates a red `(255, 0, 0)` light
     * that shines in the direction the `lightDir` vector points.
     *
     * The third way to call `directionalLight()` has four parameters. The first
     * parameter, `color`, sets the light’s color using a
     * <a href="#/p5.Color">p5.Color</a> object or an array of color values. The
     * last three parameters, `x`, `y`, and `z`, set the light’s direction. For
     * example, `directionalLight(myColor, 1, 0, 0)` creates a light that shines
     * to the right `(1, 0, 0)` with the color value of `myColor`.
     *
     * The fourth way to call `directionalLight()` has two parameters. The first
     * parameter, `color`, sets the light’s color using a
     * <a href="#/p5.Color">p5.Color</a> object or an array of color values. The
     * second parameter, `direction`, sets the light’s direction using a
     * <a href="#/p5.Vector">p5.Vector</a> object. For example,
     * `directionalLight(myColor, lightDir)` creates a light that shines in the
     * direction the `lightDir` vector points with the color value of `myColor`.
     *
     * @method directionalLight
     * @param  {Number}    v1 red or hue value in the current
     *                        <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number}    v2 green or saturation value in the current
     *                        <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number}    v3 blue, brightness, or lightness value in the current
     *                        <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number}    x  x-component of the light's direction between -1 and 1.
     * @param  {Number}    y  y-component of the light's direction between -1 and 1.
     * @param  {Number}    z  z-component of the light's direction between -1 and 1.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     * // Double-click to turn on the directional light.
     *
     * let isLit = false;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A sphere drawn on a gray background. A red light starts shining from above when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Control the light.
     *   if (isLit === true) {
     *     // Add a red directional light from above.
     *     // Use RGB values and XYZ directions.
     *     directionalLight(255, 0, 0, 0, 1, 0);
     *   }
     *
     *   // Style the sphere.
     *   noStroke();
     *
     *   // Draw the sphere.
     *   sphere(30);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A sphere drawn on a gray background. The top of the sphere appears bright red. The color gets darker toward the bottom.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Add a red directional light from above.
     *   // Use a p5.Color object and XYZ directions.
     *   let c = color(255, 0, 0);
     *   directionalLight(c, 0, 1, 0);
     *
     *   // Style the sphere.
     *   noStroke();
     *
     *   // Draw the sphere.
     *   sphere(30);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A sphere drawn on a gray background. The top of the sphere appears bright red. The color gets darker toward the bottom.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Add a red directional light from above.
     *   // Use a p5.Color object and a p5.Vector object.
     *   let c = color(255, 0, 0);
     *   let lightDir = createVector(0, 1, 0);
     *   directionalLight(c, lightDir);
     *
     *   // Style the sphere.
     *   noStroke();
     *
     *   // Draw the sphere.
     *   sphere(30);
     * }
     */
    /**
     * @method directionalLight
     * @param  {Number}    v1
     * @param  {Number}    v2
     * @param  {Number}    v3
     * @param  {p5.Vector} direction direction of the light as a
     *                               <a href="#/p5.Vector">p5.Vector</a> object.
     * @chainable
     */
    /**
     * @method directionalLight
     * @param  {p5.Color|Number[]|String} color color as a <a href="#/p5.Color">p5.Color</a> object,
     *                                           an array of color values, or as a CSS string.
     * @param  {Number}                   x
     * @param  {Number}                   y
     * @param  {Number}                   z
     * @chainable
     */
    /**
     * @method directionalLight
     * @param  {p5.Color|Number[]|String} color
     * @param  {p5.Vector}                direction
     * @chainable
     */
    fn.directionalLight = function (v1, v2, v3, x, y, z) {
      this._assert3d('directionalLight');
      // p5._validateParameters('directionalLight', arguments);

      //@TODO: check parameters number
      this._renderer.directionalLight(...arguments);

      return this;
    };

    /**
     * Creates a light that shines from a point in all directions.
     *
     * Point lights are like light bulbs that shine in all directions. They can be
     * placed at different positions to achieve different lighting effects. A
     * maximum of 5 point lights can be active at once.
     *
     * There are four ways to call `pointLight()` with parameters to set the
     * light’s color and position.
     *
     * The first way to call `pointLight()` has six parameters. The first three
     * parameters, `v1`, `v2`, and `v3`, set the light’s color using the current
     * <a href="#/p5/colorMode">colorMode()</a>. The last three parameters, `x`,
     * `y`, and `z`, set the light’s position. For example,
     * `pointLight(255, 0, 0, 50, 0, 0)` creates a red `(255, 0, 0)` light that
     * shines from the coordinates `(50, 0, 0)`.
     *
     * The second way to call `pointLight()` has four parameters. The first three
     * parameters, `v1`, `v2`, and `v3`, set the light’s color using the current
     * <a href="#/p5/colorMode">colorMode()</a>. The last parameter, position sets
     * the light’s position using a <a href="#/p5.Vector">p5.Vector</a> object.
     * For example, `pointLight(255, 0, 0, lightPos)` creates a red `(255, 0, 0)`
     * light that shines from the position set by the `lightPos` vector.
     *
     * The third way to call `pointLight()` has four parameters. The first
     * parameter, `color`, sets the light’s color using a
     * <a href="#/p5.Color">p5.Color</a> object or an array of color values. The
     * last three parameters, `x`, `y`, and `z`, set the light’s position. For
     * example, `directionalLight(myColor, 50, 0, 0)` creates a light that shines
     * from the coordinates `(50, 0, 0)` with the color value of `myColor`.
     *
     * The fourth way to call `pointLight()` has two parameters. The first
     * parameter, `color`, sets the light’s color using a
     * <a href="#/p5.Color">p5.Color</a> object or an array of color values. The
     * second parameter, `position`, sets the light’s position using a
     * <a href="#/p5.Vector">p5.Vector</a> object. For example,
     * `directionalLight(myColor, lightPos)` creates a light that shines from the
     * position set by the `lightPos` vector with the color value of `myColor`.
     *
     * @method pointLight
     * @param  {Number}    v1 red or hue value in the current
     *                        <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number}    v2 green or saturation value in the current
     *                        <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number}    v3 blue, brightness, or lightness value in the current
     *                        <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number}    x  x-coordinate of the light.
     * @param  {Number}    y  y-coordinate of the light.
     * @param  {Number}    z  z-coordinate of the light.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     * // Double-click to turn on the point light.
     *
     * let isLit = false;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A sphere drawn on a gray background. A red light starts shining from above when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Control the light.
     *   if (isLit === true) {
     *     // Add a red point light from above.
     *     // Use RGB values and XYZ coordinates.
     *     pointLight(255, 0, 0, 0, -150, 0);
     *   }
     *
     *   // Style the sphere.
     *   noStroke();
     *
     *   // Draw the sphere.
     *   sphere(30);
     * }
     *
     * // Turn on the point light when the user double-clicks.
     * function doubleClicked() {
     *   isLit = true;
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A sphere drawn on a gray background. The top of the sphere appears bright red. The color gets darker toward the bottom.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Add a red point light from above.
     *   // Use a p5.Color object and XYZ directions.
     *   let c = color(255, 0, 0);
     *   pointLight(c, 0, -150, 0);
     *
     *   // Style the sphere.
     *   noStroke();
     *
     *   // Draw the sphere.
     *   sphere(30);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A sphere drawn on a gray background. The top of the sphere appears bright red. The color gets darker toward the bottom.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Add a red point light from above.
     *   // Use a p5.Color object and a p5.Vector object.
     *   let c = color(255, 0, 0);
     *   let lightPos = createVector(0, -150, 0);
     *   pointLight(c, lightPos);
     *
     *   // Style the sphere.
     *   noStroke();
     *
     *   // Draw the sphere.
     *   sphere(30);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('Four spheres arranged in a square and drawn on a gray background. The spheres appear bright red toward the center of the square. The color gets darker toward the corners of the square.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Add a red point light that points to the center of the scene.
     *   // Use a p5.Color object and a p5.Vector object.
     *   let c = color(255, 0, 0);
     *   let lightPos = createVector(0, 0, 65);
     *   pointLight(c, lightPos);
     *
     *   // Style the spheres.
     *   noStroke();
     *
     *   // Draw a sphere up and to the left.
     *   push();
     *   translate(-25, -25, 25);
     *   sphere(10);
     *   pop();
     *
     *   // Draw a box up and to the right.
     *   push();
     *   translate(25, -25, 25);
     *   sphere(10);
     *   pop();
     *
     *   // Draw a sphere down and to the left.
     *   push();
     *   translate(-25, 25, 25);
     *   sphere(10);
     *   pop();
     *
     *   // Draw a box down and to the right.
     *   push();
     *   translate(25, 25, 25);
     *   sphere(10);
     *   pop();
     * }
     */
    /**
     * @method pointLight
     * @param  {Number}     v1
     * @param  {Number}     v2
     * @param  {Number}     v3
     * @param  {p5.Vector}  position position of the light as a
     *                               <a href="#/p5.Vector">p5.Vector</a> object.
     * @chainable
     */
    /**
     * @method pointLight
     * @param  {p5.Color|Number[]|String} color color as a <a href="#/p5.Color">p5.Color</a> object,
     *                                          an array of color values, or a CSS string.
     * @param  {Number}                   x
     * @param  {Number}                   y
     * @param  {Number}                   z
     * @chainable
     */
    /**
     * @method pointLight
     * @param  {p5.Color|Number[]|String} color
     * @param  {p5.Vector}                position
     * @chainable
     */
    fn.pointLight = function (v1, v2, v3, x, y, z) {
      this._assert3d('pointLight');
      // p5._validateParameters('pointLight', arguments);

      //@TODO: check parameters number
      this._renderer.pointLight(...arguments);

      return this;
    };

    /**
     * Creates an ambient light from an image.
     *
     * `imageLight()` simulates a light shining from all directions. The effect is
     * like placing the sketch at the center of a giant sphere that uses the image
     * as its texture. The image's diffuse light will be affected by
     * <a href="#/p5/fill">fill()</a> and the specular reflections will be
     * affected by <a href="#/p5/specularMaterial">specularMaterial()</a> and
     * <a href="#/p5/shininess">shininess()</a>.
     *
     * The parameter, `img`, is the <a href="#/p5.Image">p5.Image</a> object to
     * use as the light source.
     *
     * @method imageLight
     * @param  {p5.Image}    img image to use as the light source.
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let img;
     *
     * async function setup() {
     *   // Load an image and create a p5.Image object.
     *   img = await loadImage('assets/outdoor_spheremap.jpg');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A sphere floating above a landscape. The surface of the sphere reflects the landscape.');
     * }
     *
     * function draw() {
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the image as a panorama (360˚ background).
     *   panorama(img);
     *
     *   // Add a soft ambient light.
     *   ambientLight(50);
     *
     *   // Add light from the image.
     *   imageLight(img);
     *
     *   // Style the sphere.
     *   specularMaterial(20);
     *   shininess(100);
     *   noStroke();
     *
     *   // Draw the sphere.
     *   sphere(30);
     * }
     */
    fn.imageLight = function (img) {
      this._renderer.imageLight(img);
    };

    /**
     * Creates an immersive 3D background.
     *
     * `panorama()` transforms images containing 360˚ content, such as maps or
     * HDRIs, into immersive 3D backgrounds that surround a sketch. Exploring the
     * space requires changing the camera's perspective with functions such as
     * <a href="#/p5/orbitControl">orbitControl()</a> or
     * <a href="#/p5/camera">camera()</a>.
     *
     * @method panorama
     * @param {p5.Image} img 360˚ image to use as the background.
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let img;
     *
     * async function setup() {
     *   // Load an image and create a p5.Image object.
     *   img = await loadImage('assets/outdoor_spheremap.jpg');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A sphere floating above a landscape. The surface of the sphere reflects the landscape. The full landscape is viewable in 3D as the user drags the mouse.');
     * }
     *
     * function draw() {
     *   // Add the panorama.
     *   panorama(img);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Use the image as a light source.
     *   imageLight(img);
     *
     *   // Style the sphere.
     *   noStroke();
     *   specularMaterial(50);
     *   shininess(200);
     *   metalness(100);
     *
     *   // Draw the sphere.
     *   sphere(30);
     * }
     */
    fn.panorama = function (img) {
      this.filter(this._renderer._getSphereMapping(img));
    };

    /**
     * Places an ambient and directional light in the scene.
     * The lights are set to ambientLight(128, 128, 128) and
     * directionalLight(128, 128, 128, 0, 0, -1).
     *
     * Note: lights need to be called (whether directly or indirectly)
     * within draw() to remain persistent in a looping program.
     * Placing them in setup() will cause them to only have an effect
     * the first time through the loop.
     *
     * @method lights
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     * // Double-click to turn on the lights.
     *
     * let isLit = false;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white box drawn against a gray background. The quality of the light changes when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Control the lights.
     *   if (isLit === true) {
     *     lights();
     *   }
     *
     *   // Draw the box.
     *   box();
     * }
     *
     * // Turn on the lights when the user double-clicks.
     * function doubleClicked() {
     *   isLit = true;
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white box drawn against a gray background.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   ambientLight(128, 128, 128);
     *   directionalLight(128, 128, 128, 0, 0, -1);
     *
     *   // Draw the box.
     *   box();
     * }
     */
    fn.lights = function () {
      this._assert3d('lights');
      // Both specify gray by default.
      this._renderer.lights();
      return this;
    };

    /**
     * Sets the falloff rate for <a href="#/p5/pointLight">pointLight()</a>
     * and <a href="#/p5/spotLight">spotLight()</a>.
     *
     * A light’s falloff describes the intensity of its beam at a distance. For
     * example, a lantern has a slow falloff, a flashlight has a medium falloff,
     * and a laser pointer has a sharp falloff.
     *
     * `lightFalloff()` has three parameters, `constant`, `linear`, and
     * `quadratic`. They’re numbers used to calculate falloff at a distance, `d`,
     * as follows:
     *
     * `falloff = 1 / (constant + d * linear + (d * d) * quadratic)`
     *
     * Note: `constant`, `linear`, and `quadratic` should always be set to values
     * greater than 0.
     *
     * @method lightFalloff
     * @param {Number} constant  constant value for calculating falloff.
     * @param {Number} linear    linear value for calculating falloff.
     * @param {Number} quadratic quadratic value for calculating falloff.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     * // Double-click to change the falloff rate.
     *
     * let useFalloff = false;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A sphere drawn against a gray background. The intensity of the light changes when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Set the light falloff.
     *   if (useFalloff === true) {
     *     lightFalloff(2, 0, 0);
     *   }
     *
     *   // Add a white point light from the front.
     *   pointLight(255, 255, 255, 0, 0, 100);
     *
     *   // Style the sphere.
     *   noStroke();
     *
     *   // Draw the sphere.
     *   sphere(30);
     * }
     *
     * // Change the falloff value when the user double-clicks.
     * function doubleClicked() {
     *   useFalloff = true;
     * }
     */
    fn.lightFalloff = function (
      constantAttenuation,
      linearAttenuation,
      quadraticAttenuation
    ) {
      this._assert3d('lightFalloff');
      // p5._validateParameters('lightFalloff', arguments);

      this._renderer.lightFalloff(
        constantAttenuation,
        linearAttenuation,
        quadraticAttenuation
      );

      return this;
    };

    /**
     * Creates a light that shines from a point in one direction.
     *
     * Spot lights are like flashlights that shine in one direction creating a
     * cone of light. The shape of the cone can be controlled using the angle and
     * concentration parameters. A maximum of 5 spot lights can be active at once.
     *
     * There are eight ways to call `spotLight()` with parameters to set the
     * light’s color, position, direction. For example,
     * `spotLight(255, 0, 0, 0, 0, 0, 1, 0, 0)` creates a red `(255, 0, 0)` light
     * at the origin `(0, 0, 0)` that points to the right `(1, 0, 0)`.
     *
     * The `angle` parameter is optional. It sets the radius of the light cone.
     * For example, `spotLight(255, 0, 0, 0, 0, 0, 1, 0, 0, PI / 16)` creates a
     * red `(255, 0, 0)` light at the origin `(0, 0, 0)` that points to the right
     * `(1, 0, 0)` with an angle of `PI / 16` radians. By default, `angle` is
     * `PI / 3` radians.
     *
     * The `concentration` parameter is also optional. It focuses the light
     * towards the center of the light cone. For example,
     * `spotLight(255, 0, 0, 0, 0, 0, 1, 0, 0, PI / 16, 50)` creates a red
     * `(255, 0, 0)` light at the origin `(0, 0, 0)` that points to the right
     * `(1, 0, 0)` with an angle of `PI / 16` radians at concentration of 50. By
     * default, `concentration` is 100.
     *
     * @method spotLight
     * @param  {Number}    v1               red or hue value in the current
     *                                      <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number}    v2               green or saturation value in the current
     *                                      <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number}    v3               blue, brightness, or lightness value in the current
     *                                      <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number}    x                x-coordinate of the light.
     * @param  {Number}    y                y-coordinate of the light.
     * @param  {Number}    z                z-coordinate of the light.
     * @param  {Number}    rx               x-component of light direction between -1 and 1.
     * @param  {Number}    ry               y-component of light direction between -1 and 1.
     * @param  {Number}    rz               z-component of light direction between -1 and 1.
     * @param  {Number}    [angle]          angle of the light cone. Defaults to `PI / 3`.
     * @param  {Number}    [concentration]  concentration of the light. Defaults to 100.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     * // Double-click to adjust the spotlight.
     *
     * let isLit = false;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white sphere drawn on a gray background. A red spotlight starts shining when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Control the spotlight.
     *   if (isLit === true) {
     *     // Add a red spot light that shines into the screen.
     *     // Set its angle to PI / 32 radians.
     *     spotLight(255, 0, 0, 0, 0, 100, 0, 0, -1, PI / 32);
     *   }
     *
     *   // Draw the sphere.
     *   sphere(30);
     * }
     *
     * // Turn on the spotlight when the user double-clicks.
     * function doubleClicked() {
     *   isLit = true;
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     * // Double-click to adjust the spotlight.
     *
     * let isLit = false;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white sphere drawn on a gray background. A red spotlight starts shining when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Control the spotlight.
     *   if (isLit === true) {
     *     // Add a red spot light that shines into the screen.
     *     // Set its angle to PI / 3 radians (default).
     *     // Set its concentration to 1000.
     *     let c = color(255, 0, 0);
     *     let position = createVector(0, 0, 100);
     *     let direction = createVector(0, 0, -1);
     *     spotLight(c, position, direction, PI / 3, 1000);
     *   }
     *
     *   // Draw the sphere.
     *   sphere(30);
     * }
     *
     * // Turn on the spotlight when the user double-clicks.
     * function doubleClicked() {
     *   isLit = true;
     * }
     */
    /**
     * @method spotLight
     * @param  {p5.Color|Number[]|String} color     color as a <a href="#/p5.Color">p5.Color</a> object,
     *                                              an array of color values, or a CSS string.
     * @param  {p5.Vector}                position  position of the light as a <a href="#/p5.Vector">p5.Vector</a> object.
     * @param  {p5.Vector}                direction direction of light as a <a href="#/p5.Vector">p5.Vector</a> object.
     * @param  {Number}                   [angle]
     * @param  {Number}                   [concentration]
     */
    /**
     * @method spotLight
     * @param  {Number}     v1
     * @param  {Number}     v2
     * @param  {Number}     v3
     * @param  {p5.Vector}  position
     * @param  {p5.Vector}  direction
     * @param  {Number}     [angle]
     * @param  {Number}     [concentration]
     */
    /**
     * @method spotLight
     * @param  {p5.Color|Number[]|String} color
     * @param  {Number}                   x
     * @param  {Number}                   y
     * @param  {Number}                   z
     * @param  {p5.Vector}                direction
     * @param  {Number}                   [angle]
     * @param  {Number}                   [concentration]
     */
    /**
     * @method spotLight
     * @param  {p5.Color|Number[]|String} color
     * @param  {p5.Vector}                position
     * @param  {Number}                   rx
     * @param  {Number}                   ry
     * @param  {Number}                   rz
     * @param  {Number}                   [angle]
     * @param  {Number}                   [concentration]
     */
    /**
     * @method spotLight
     * @param  {Number}     v1
     * @param  {Number}     v2
     * @param  {Number}     v3
     * @param  {Number}     x
     * @param  {Number}     y
     * @param  {Number}     z
     * @param  {p5.Vector}  direction
     * @param  {Number}     [angle]
     * @param  {Number}     [concentration]
     */
    /**
     * @method spotLight
     * @param  {Number}     v1
     * @param  {Number}     v2
     * @param  {Number}     v3
     * @param  {p5.Vector}  position
     * @param  {Number}     rx
     * @param  {Number}     ry
     * @param  {Number}     rz
     * @param  {Number}     [angle]
     * @param  {Number}     [concentration]
     */
    /**
     * @method spotLight
     * @param  {p5.Color|Number[]|String} color
     * @param  {Number}                   x
     * @param  {Number}                   y
     * @param  {Number}                   z
     * @param  {Number}                   rx
     * @param  {Number}                   ry
     * @param  {Number}                   rz
     * @param  {Number}                   [angle]
     * @param  {Number}                   [concentration]
     */
    fn.spotLight = function (
      v1,
      v2,
      v3,
      x,
      y,
      z,
      nx,
      ny,
      nz,
      angle,
      concentration
    ) {
      this._assert3d('spotLight');
      // p5._validateParameters('spotLight', arguments);

      this._renderer.spotLight(...arguments);

      return this;
    };

    /**
     * Removes all lights from the sketch.
     *
     * Calling `noLights()` removes any lights created with
     * <a href="#/p5/lights">lights()</a>,
     * <a href="#/p5/ambientLight">ambientLight()</a>,
     * <a href="#/p5/directionalLight">directionalLight()</a>,
     * <a href="#/p5/pointLight">pointLight()</a>, or
     * <a href="#/p5/spotLight">spotLight()</a>. These functions may be called
     * after `noLights()` to create a new lighting scheme.
     *
     * @method noLights
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('Two spheres drawn against a gray background. The top sphere is white and the bottom sphere is red.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Style the spheres.
     *   noStroke();
     *
     *   // Draw the top sphere.
     *   push();
     *   translate(0, -25, 0);
     *   sphere(20);
     *   pop();
     *
     *   // Turn off the lights.
     *   noLights();
     *
     *   // Add a red directional light that points into the screen.
     *   directionalLight(255, 0, 0, 0, 0, -1);
     *
     *   // Draw the bottom sphere.
     *   push();
     *   translate(0, 25, 0);
     *   sphere(20);
     *   pop();
     * }
     */
    fn.noLights = function (...args) {
      this._assert3d('noLights');
      // p5._validateParameters('noLights', args);

      this._renderer.noLights();

      return this;
    };


    Renderer3D.prototype.ambientLight = function(v1, v2, v3, a) {
      const color = this._pInst.color(...arguments);

      this.states.setValue('ambientLightColors', [...this.states.ambientLightColors]);
      this.states.ambientLightColors.push(
        color._array[0],
        color._array[1],
        color._array[2]
      );

      this.states.setValue('enableLighting', true);
    };

    Renderer3D.prototype.specularColor = function(v1, v2, v3) {
      const color = this._pInst.color(...arguments);

      this.states.setValue('specularColors', [
        color._array[0],
        color._array[1],
        color._array[2]
      ]);
    };

    Renderer3D.prototype.directionalLight = function(v1, v2, v3, x, y, z) {
      let color;
      if (v1 instanceof Color) {
        color = v1;
      } else {
        color = this._pInst.color(v1, v2, v3);
      }

      let _x, _y, _z;
      const v = arguments[arguments.length - 1];
      if (typeof v === 'number') {
        _x = arguments[arguments.length - 3];
        _y = arguments[arguments.length - 2];
        _z = arguments[arguments.length - 1];
      } else {
        _x = v.x;
        _y = v.y;
        _z = v.z;
      }

      // normalize direction
      const l = Math.sqrt(_x * _x + _y * _y + _z * _z);
      this.states.setValue('directionalLightDirections', [...this.states.directionalLightDirections]);
      this.states.directionalLightDirections.push(_x / l, _y / l, _z / l);

      this.states.setValue('directionalLightDiffuseColors', [...this.states.directionalLightDiffuseColors]);
      this.states.directionalLightDiffuseColors.push(
        color._array[0],
        color._array[1],
        color._array[2]
      );

      this.states.setValue('directionalLightSpecularColors', [...this.states.directionalLightSpecularColors]);
      Array.prototype.push.apply(
        this.states.directionalLightSpecularColors,
        this.states.specularColors
      );

      this.states.setValue('enableLighting', true);
    };

    Renderer3D.prototype.pointLight = function(v1, v2, v3, x, y, z) {
      let color;
      if (v1 instanceof Color) {
        color = v1;
      } else {
        color = this._pInst.color(v1, v2, v3);
      }

      let _x, _y, _z;
      const v = arguments[arguments.length - 1];
      if (typeof v === 'number') {
        _x = arguments[arguments.length - 3];
        _y = arguments[arguments.length - 2];
        _z = arguments[arguments.length - 1];
      } else {
        _x = v.x;
        _y = v.y;
        _z = v.z;
      }

      this.states.setValue('pointLightPositions', [...this.states.pointLightPositions]);
      this.states.pointLightPositions.push(_x, _y, _z);

      this.states.setValue('pointLightDiffuseColors', [...this.states.pointLightDiffuseColors]);
      this.states.pointLightDiffuseColors.push(
        color._array[0],
        color._array[1],
        color._array[2]
      );

      this.states.setValue('pointLightSpecularColors', [...this.states.pointLightSpecularColors]);
      Array.prototype.push.apply(
        this.states.pointLightSpecularColors,
        this.states.specularColors
      );

      this.states.setValue('enableLighting', true);
    };

    Renderer3D.prototype.imageLight = function(img) {
      // activeImageLight property is checked by _setFillUniforms
      // for sending uniforms to the fillshader
      this.states.setValue('activeImageLight', img);
      this.states.setValue('enableLighting', true);
      // Make sure textures are cached
      this.makeDiffusedTexture(img);
      this.makeSpecularTexture(img);
    };

    Renderer3D.prototype.lights = function() {
      const grayColor = this._pInst.color('rgb(128,128,128)');
      this.ambientLight(grayColor);
      this.directionalLight(grayColor, 0, 0, -1);
    };

    Renderer3D.prototype.lightFalloff = function(
      constantAttenuation,
      linearAttenuation,
      quadraticAttenuation
    ) {
      if (constantAttenuation < 0) {
        constantAttenuation = 0;
        console.warn(
          'Value of constant argument in lightFalloff() should be never be negative. Set to 0.'
        );
      }

      if (linearAttenuation < 0) {
        linearAttenuation = 0;
        console.warn(
          'Value of linear argument in lightFalloff() should be never be negative. Set to 0.'
        );
      }

      if (quadraticAttenuation < 0) {
        quadraticAttenuation = 0;
        console.warn(
          'Value of quadratic argument in lightFalloff() should be never be negative. Set to 0.'
        );
      }

      if (
        constantAttenuation === 0 &&
        (linearAttenuation === 0 && quadraticAttenuation === 0)
      ) {
        constantAttenuation = 1;
        console.warn(
          'Either one of the three arguments in lightFalloff() should be greater than zero. Set constant argument to 1.'
        );
      }

      this.states.setValue('constantAttenuation', constantAttenuation);
      this.states.setValue('linearAttenuation', linearAttenuation);
      this.states.setValue('quadraticAttenuation', quadraticAttenuation);
    };

    Renderer3D.prototype.spotLight = function(
      v1,
      v2,
      v3,
      x,
      y,
      z,
      nx,
      ny,
      nz,
      angle,
      concentration
    ) {
      if (this.states.spotLightDiffuseColors.length / 3 >= 4) return;

      let color, position, direction;
      const length = arguments.length;

      switch (length) {
        case 11:
        case 10:
          color = this._pInst.color(v1, v2, v3);
          position = new Vector(x, y, z);
          direction = new Vector(nx, ny, nz);
          break;

        case 9:
          if (v1 instanceof Color) {
            color = v1;
            position = new Vector(v2, v3, x);
            direction = new Vector(y, z, nx);
            angle = ny;
            concentration = nz;
          } else if (x instanceof Vector) {
            color = this._pInst.color(v1, v2, v3);
            position = x;
            direction = new Vector(y, z, nx);
            angle = ny;
            concentration = nz;
          } else if (nx instanceof Vector) {
            color = this._pInst.color(v1, v2, v3);
            position = new Vector(x, y, z);
            direction = nx;
            angle = ny;
            concentration = nz;
          } else {
            color = this._pInst.color(v1, v2, v3);
            position = new Vector(x, y, z);
            direction = new Vector(nx, ny, nz);
          }
          break;

        case 8:
          if (v1 instanceof Color) {
            color = v1;
            position = new Vector(v2, v3, x);
            direction = new Vector(y, z, nx);
            angle = ny;
          } else if (x instanceof Vector) {
            color = this._pInst.color(v1, v2, v3);
            position = x;
            direction = new Vector(y, z, nx);
            angle = ny;
          } else {
            color = this._pInst.color(v1, v2, v3);
            position = new Vector(x, y, z);
            direction = nx;
            angle = ny;
          }
          break;

        case 7:
          if (v1 instanceof Color && v2 instanceof Vector) {
            color = v1;
            position = v2;
            direction = new Vector(v3, x, y);
            angle = z;
            concentration = nx;
          } else if (v1 instanceof Color && y instanceof Vector) {
            color = v1;
            position = new Vector(v2, v3, x);
            direction = y;
            angle = z;
            concentration = nx;
          } else if (x instanceof Vector && y instanceof Vector) {
            color = this._pInst.color(v1, v2, v3);
            position = x;
            direction = y;
            angle = z;
            concentration = nx;
          } else if (v1 instanceof Color) {
            color = v1;
            position = new Vector(v2, v3, x);
            direction = new Vector(y, z, nx);
          } else if (x instanceof Vector) {
            color = this._pInst.color(v1, v2, v3);
            position = x;
            direction = new Vector(y, z, nx);
          } else {
            color = this._pInst.color(v1, v2, v3);
            position = new Vector(x, y, z);
            direction = nx;
          }
          break;

        case 6:
          if (x instanceof Vector && y instanceof Vector) {
            color = this._pInst.color(v1, v2, v3);
            position = x;
            direction = y;
            angle = z;
          } else if (v1 instanceof Color && y instanceof Vector) {
            color = v1;
            position = new Vector(v2, v3, x);
            direction = y;
            angle = z;
          } else if (v1 instanceof Color && v2 instanceof Vector) {
            color = v1;
            position = v2;
            direction = new Vector(v3, x, y);
            angle = z;
          }
          break;

        case 5:
          if (
            v1 instanceof Color &&
            v2 instanceof Vector &&
            v3 instanceof Vector
          ) {
            color = v1;
            position = v2;
            direction = v3;
            angle = x;
            concentration = y;
          } else if (x instanceof Vector && y instanceof Vector) {
            color = this._pInst.color(v1, v2, v3);
            position = x;
            direction = y;
          } else if (v1 instanceof Color && y instanceof Vector) {
            color = v1;
            position = new Vector(v2, v3, x);
            direction = y;
          } else if (v1 instanceof Color && v2 instanceof Vector) {
            color = v1;
            position = v2;
            direction = new Vector(v3, x, y);
          }
          break;

        case 4:
          color = v1;
          position = v2;
          direction = v3;
          angle = x;
          break;

        case 3:
          color = v1;
          position = v2;
          direction = v3;
          break;

        default:
          console.warn(
            `Sorry, input for spotlight() is not in prescribed format. Too ${
            length < 3 ? 'few' : 'many'
          } arguments were provided`
          );
          return;
      }
      this.states.setValue('spotLightDiffuseColors', [
        ...this.states.spotLightDiffuseColors,
        color._array[0],
        color._array[1],
        color._array[2]
      ]);

      this.states.setValue('spotLightSpecularColors', [
        ...this.states.spotLightSpecularColors,
        ...this.states.specularColors
      ]);

      this.states.setValue('spotLightPositions', [
        ...this.states.spotLightPositions,
        position.x,
        position.y,
        position.z
      ]);
      direction.normalize();
      this.states.setValue('spotLightDirections', [
        ...this.states.spotLightDirections,
        direction.x,
        direction.y,
        direction.z
      ]);

      if (angle === undefined) {
        angle = Math.PI / 3;
      }

      if (concentration !== undefined && concentration < 1) {
        concentration = 1;
        console.warn(
          'Value of concentration needs to be greater than 1. Setting it to 1'
        );
      } else if (concentration === undefined) {
        concentration = 100;
      }

      angle = this._pInst._toRadians(angle);
      this.states.setValue('spotLightAngle', [...this.states.spotLightAngle, Math.cos(angle)]);
      this.states.setValue('spotLightConc', [...this.states.spotLightConc, concentration]);

      this.states.setValue('enableLighting', true);
    };

    Renderer3D.prototype.noLights = function() {
      this.states.setValue('activeImageLight', null);
      this.states.setValue('enableLighting', false);

      this.states.setValue('ambientLightColors', []);
      this.states.setValue('specularColors', [1, 1, 1]);

      this.states.setValue('directionalLightDirections', []);
      this.states.setValue('directionalLightDiffuseColors', []);
      this.states.setValue('directionalLightSpecularColors', []);

      this.states.setValue('pointLightPositions', []);
      this.states.setValue('pointLightDiffuseColors', []);
      this.states.setValue('pointLightSpecularColors', []);

      this.states.setValue('spotLightPositions', []);
      this.states.setValue('spotLightDirections', []);
      this.states.setValue('spotLightDiffuseColors', []);
      this.states.setValue('spotLightSpecularColors', []);
      this.states.setValue('spotLightAngle', []);
      this.states.setValue('spotLightConc', []);

      this.states.setValue('constantAttenuation', 1);
      this.states.setValue('linearAttenuation', 0);
      this.states.setValue('quadraticAttenuation', 0);
      this.states.setValue('_useShininess', 1);
      this.states.setValue('_useMetalness', 0);
    };
  }

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

  /**
   * This module defines the p5.Shader class
   * @module 3D
   * @submodule Material
   * @for p5
   * @requires core
   */

  const TypedArray = Object.getPrototypeOf(Uint8Array);
  class Shader {
    constructor(renderer, vertSrc, fragSrc, options = {}) {
      this._renderer = renderer;
      this._vertSrc = vertSrc;
      this._fragSrc = fragSrc;
      this._vertShader = -1;
      this._fragShader = -1;
      this._glProgram = 0;
      this._loadedAttributes = false;
      this.attributes = {};
      this._loadedUniforms = false;
      this.uniforms = {};
      this._bound = false;
      this.samplers = [];
      this.hooks = {
        // These should be passed in by `.modify()` instead of being manually
        // passed in.

        // Stores uniforms + default values.
        uniforms: options.uniforms || {},

        // Stores custom uniform + helper declarations as a string.
        declarations: options.declarations,

        // Stores an array of variable names + types passed between the vertex and fragment shader
        varyingVariables: options.varyingVariables || [],

        // Stores helper functions to prepend to shaders.
        helpers: options.helpers || {},

        // Stores the hook implementations
        vertex: options.vertex || {},
        fragment: options.fragment || {},

        hookAliases: options.hookAliases || {},

        // Stores whether or not the hook implementation has been modified
        // from the default. This is supplied automatically by calling
        // yourShader.modify(...).
        modified: {
          vertex: (options.modified && options.modified.vertex) || {},
          fragment: (options.modified && options.modified.fragment) || {}
        }
      };
    }

    hookTypes(hookName) {
      return this._renderer.getShaderHookTypes(this, hookName);
    }

    shaderSrc(src, shaderType) {
      return this._renderer.populateHooks(this, src, shaderType);
    }

    /**
     * Shaders are written in <a href="https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_on_the_web/GLSL_Shaders">GLSL</a>, but
     * there are different versions of GLSL that it might be written in.
     *
     * Calling this method on a `p5.Shader` will return the GLSL version it uses, either `100 es` or `300 es`.
     * WebGL 1 shaders will only use `100 es`, and WebGL 2 shaders may use either.
     *
     * @returns {String} The GLSL version used by the shader.
     */
    version() {
      const match = /#version (.+)$/.exec(this.vertSrc());
      if (match) {
        return match[1];
      } else {
        return '100 es';
      }
    }

    vertSrc() {
      return this.shaderSrc(this._vertSrc, 'vertex');
    }

    fragSrc() {
      return this.shaderSrc(this._fragSrc, 'fragment');
    }

    /**
     * Logs the hooks available in this shader, and their current implementation.
     *
     * Each shader may let you override bits of its behavior. Each bit is called
     * a *hook.* A hook is either for the *vertex* shader, if it affects the
     * position of vertices, or in the *fragment* shader, if it affects the pixel
     * color. This method logs those values to the console, letting you know what
     * you are able to use in a call to
     * <a href="#/p5.Shader/modify">`modify()`</a>.
     *
     * For example, this shader will produce the following output:
     *
     * ```js
     * myShader = baseMaterialShader().modify({
     *   declarations: 'uniform float time;',
     *   'vec3 getWorldPosition': `(vec3 pos) {
     *     pos.y += 20. * sin(time * 0.001 + pos.x * 0.05);
     *     return pos;
     *   }`
     * });
     * myShader.inspectHooks();
     * ```
     *
     * ```
     * ==== Vertex shader hooks: ====
     * void beforeVertex() {}
     * vec3 getLocalPosition(vec3 position) { return position; }
     * [MODIFIED] vec3 getWorldPosition(vec3 pos) {
     *       pos.y += 20. * sin(time * 0.001 + pos.x * 0.05);
     *       return pos;
     *     }
     * vec3 getLocalNormal(vec3 normal) { return normal; }
     * vec3 getWorldNormal(vec3 normal) { return normal; }
     * vec2 getUV(vec2 uv) { return uv; }
     * vec4 getVertexColor(vec4 color) { return color; }
     * void afterVertex() {}
     *
     * ==== Fragment shader hooks: ====
     * void beforeFragment() {}
     * Inputs getPixelInputs(Inputs inputs) { return inputs; }
     * vec4 combineColors(ColorComponents components) {
     *                 vec4 color = vec4(0.);
     *                 color.rgb += components.diffuse * components.baseColor;
     *                 color.rgb += components.ambient * components.ambientColor;
     *                 color.rgb += components.specular * components.specularColor;
     *                 color.rgb += components.emissive;
     *                 color.a = components.opacity;
     *                 return color;
     *               }
     * vec4 getFinalColor(vec4 color) { return color; }
     * void afterFragment() {}
     * ```
     *
     * @beta
     */
    inspectHooks() {
      console.log('==== Vertex shader hooks: ====');
      for (const key in this.hooks.vertex) {
        console.log(
          (this.hooks.modified.vertex[key] ? '[MODIFIED] ' : '') +
            key +
            this.hooks.vertex[key]
        );
      }
      console.log('');
      console.log('==== Fragment shader hooks: ====');
      for (const key in this.hooks.fragment) {
        console.log(
          (this.hooks.modified.fragment[key] ? '[MODIFIED] ' : '') +
            key +
            this.hooks.fragment[key]
        );
      }
      console.log('');
      console.log('==== Helper functions: ====');
      for (const key in this.hooks.helpers) {
        console.log(key + this.hooks.helpers[key]);
      }
    }

    /**
     * Returns a new shader, based on the original, but with custom snippets
     * of shader code replacing default behaviour.
     *
     * Each shader may let you override bits of its behavior. Each bit is called
     * a *hook.* For example, a hook can let you adjust positions of vertices, or
     * the color of a pixel. You can inspect the different hooks available by calling
     * <a href="#/p5.Shader/inspectHooks">`yourShader.inspectHooks()`</a>. You can
     * also read the reference for the default material, normal material, color, line, and point shaders to
     * see what hooks they have available.
     *
     * `modify()` can be passed a function as a parameter. Inside, you can override hooks
     * by calling them as functions. Each hook will take in a callback that takes in inputs
     * and is expected to return an output. For example, here is a function that changes the
     * material color to red:
     *
     * ```js example
     * let myShader;
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = baseMaterialShader().modify(() => {
     *     getPixelInputs((inputs) => {
     *       inputs.color = [inputs.texCoord, 0, 1];
     *       return inputs;
     *     });
     *   });
     * }
     *
     * function draw() {
     *   background(255);
     *   noStroke();
     *   shader(myShader); // Apply the custom shader
     *   plane(width, height); // Draw a plane with the shader applied
     * }
     * ```
     *
     * In addition to calling hooks, you can create uniforms, which are special variables
     * used to pass data from p5.js into the shader. They can be created by calling `uniform` + the
     * type of the data, such as `uniformFloat` for a number of `uniformVector2` for a two-component vector.
     * They take in a function that returns the data for the variable. You can then reference these
     * variables in your hooks, and their values will update every time you apply
     * the shader with the result of your function.
     *
     * ```js example
     * let myShader;
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = baseMaterialShader().modify(() => {
     *     // Get the current time from p5.js
     *     let t = uniformFloat(() => millis());
     *
     *     getPixelInputs((inputs) => {
     *       inputs.color = [
     *         inputs.texCoord,
     *         sin(t * 0.01) / 2 + 0.5,
     *         1,
     *       ];
     *       return inputs;
     *     });
     *   });
     * }
     *
     * function draw() {
     *   background(255);
     *   noStroke(255);
     *   shader(myShader); // Apply the custom shader
     *   plane(width, height); // Draw a plane with the shader applied
     * }
     * ```
     *
     * p5.strands functions are special, since they get turned into a shader instead of being
     * run like the rest of your code. They only have access to p5.js functions, and variables
     * you declare inside the `modify` callback. If you need access to local variables, you
     * can pass them into `modify` with an optional second parameter, `variables`. These will
     * then be passed into your function as an argument. If you are
     * using instance mode, you will need to pass your sketch object in this way.
     *
     * If you are also using a build system for your sketch, variable names may be changed as
     * part of minification. When creating a uniform, you can pass the name of the uniform in
     * as a first parameter to ensure it doesn't get changed.
     *
     * ```js example
     * new p5((sketch) => {
     *   let myShader;
     *
     *   sketch.setup = function() {
     *     sketch.createCanvas(200, 200, sketch.WEBGL);
     *     myShader = sketch.baseMaterialShader().modify(({ sketch }) => {
     *       let b = sketch.uniformFloat('b');
     *       sketch.getPixelInputs((inputs) => {
     *         inputs.color = [inputs.texCoord, b, 1];
     *         return inputs;
     *       });
     *     }, { sketch });
     *   }
     *
     *   sketch.draw = function() {
     *     sketch.background(255);
     *     sketch.noStroke();
     *     myShader.setUniform('b', 0.5);
     *     sketch.shader(myShader); // Apply the custom shader
     *     sketch.plane(sketch.width, sketch.height); // Draw a plane with the shader applied
     *   }
     * });
     * ```
     *
     * You can also write GLSL directly in `modify` if you need direct access. To do so,
     * `modify()` takes one parameter, `hooks`, an object with the hooks you want
     * to override. Each key of the `hooks` object is the name
     * of a hook, and the value is a string with the GLSL code for your hook.
     *
     * If you supply functions that aren't existing hooks, they will get added at the start of
     * the shader as helper functions so that you can use them in your hooks.
     *
     * To add new <a href="#/p5.Shader/setUniform">uniforms</a> to your shader, you can pass in a `uniforms` object containing
     * the type and name of the uniform as the key, and a default value or function returning
     * a default value as its value. These will be automatically set when the shader is set
     * with `shader(yourShader)`.
     *
     * ```js example
     * let myShader;
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = baseMaterialShader().modify({
     *     uniforms: {
     *       'float time': () => millis() // Uniform for time
     *     },
     *     'Vertex getWorldInputs': `(Vertex inputs) {
     *       inputs.position.y +=
     *         20. * sin(time * 0.001 + inputs.position.x * 0.05);
     *       return inputs;
     *     }`
     *   });
     * }
     *
     * function draw() {
     *   background(255);
     *   shader(myShader); // Apply the custom shader
     *   lights();         // Enable lighting
     *   noStroke();       // Disable stroke
     *   fill('red');      // Set fill color to red
     *   sphere(50);       // Draw a sphere with the shader applied
     * }
     * ```
     *
     * You can also add a `declarations` key, where the value is a GLSL string declaring
     * custom uniform variables, globals, and functions shared
     * between hooks. To add declarations just in a vertex or fragment shader, add
     * `vertexDeclarations` and `fragmentDeclarations` keys.
     *
     * ```js example
     * let myShader;
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = baseMaterialShader().modify({
     *     // Manually specifying a uniform
     *     declarations: 'uniform float time;',
     *     'Vertex getWorldInputs': `(Vertex inputs) {
     *       inputs.position.y +=
     *         20. * sin(time * 0.001 + inputs.position.x * 0.05);
     *       return inputs;
     *     }`
     *   });
     * }
     *
     * function draw() {
     *   background(255);
     *   shader(myShader);
     *   myShader.setUniform('time', millis());
     *   lights();
     *   noStroke();
     *   fill('red');
     *   sphere(50);
     * }
     * ```
     *
     * @beta
     * @param {Function} callback A function with p5.strands code to modify the shader.
     * @param {Object} [variables] An optional object with local variables p5.strands
     * should have access to.
     * @returns {p5.Shader}
     */
    /**
     * @param {Object} [hooks] The hooks in the shader to replace.
     * @returns {p5.Shader}
     */
    modify(hooks) {
      // p5._validateParameters('p5.Shader.modify', arguments);
      const newHooks = {
        vertex: {},
        fragment: {},
        helpers: {}
      };
      for (const key in hooks) {
        if (key === 'declarations') continue;
        if (key === 'uniforms') continue;
        if (key === 'varyingVariables') continue;
        if (key === 'vertexDeclarations') {
          newHooks.vertex.declarations =
            (newHooks.vertex.declarations || '') + '\n' + hooks[key];
        } else if (key === 'fragmentDeclarations') {
          newHooks.fragment.declarations =
            (newHooks.fragment.declarations || '') + '\n' + hooks[key];
        } else if (this.hooks.vertex[key]) {
          newHooks.vertex[key] = hooks[key];
        } else if (this.hooks.fragment[key]) {
          newHooks.fragment[key] = hooks[key];
        } else {
          newHooks.helpers[key] = hooks[key];
        }
      }
      const modifiedVertex = Object.assign({}, this.hooks.modified.vertex);
      const modifiedFragment = Object.assign({}, this.hooks.modified.fragment);
      for (const key in newHooks.vertex || {}) {
        if (key === 'declarations') continue;
        modifiedVertex[key] = true;
      }
      for (const key in newHooks.fragment || {}) {
        if (key === 'declarations') continue;
        modifiedFragment[key] = true;
      }

      return new Shader(this._renderer, this._vertSrc, this._fragSrc, {
        declarations:
          (this.hooks.declarations || '') + '\n' + (hooks.declarations || ''),
        uniforms: Object.assign({}, this.hooks.uniforms, hooks.uniforms || {}),
        varyingVariables: (hooks.varyingVariables || []).concat(this.hooks.varyingVariables || []),
        fragment: Object.assign({}, this.hooks.fragment, newHooks.fragment || {}),
        vertex: Object.assign({}, this.hooks.vertex, newHooks.vertex || {}),
        helpers: Object.assign({}, this.hooks.helpers, newHooks.helpers || {}),
        hookAliases: Object.assign({}, this.hooks.hookAliases, newHooks.hookAliases || {}),
        modified: {
          vertex: modifiedVertex,
          fragment: modifiedFragment
        }
      });
    }

    /**
     * Creates, compiles, and links the shader based on its
     * sources for the vertex and fragment shaders (provided
     * to the constructor). Populates known attributes and
     * uniforms from the shader.
     * @chainable
     * @private
     */
    init() {
      // If the shader is uninitialized or context was lost
      if (!this._initialized) {
        try {
          this._renderer._initShader(this); // Backend-specific shader init
        } catch (err) {
          throw new Error(
            `Whoops! Something went wrong initializing the shader:\n${err.message || err}`
          );
        }

        this._loadAttributes();
        this._loadUniforms();
        this._renderer._finalizeShader(this);

        this._initialized = true;
      }

      return this;
    }

    /**
     * @private
     */
    setDefaultUniforms() {
      for (const key in this.hooks.uniforms) {
        const name = this._renderer.uniformNameFromHookKey(key);
        const initializer = this.hooks.uniforms[key];
        let value;
        if (initializer instanceof Function) {
          value = initializer();
        } else {
          value = initializer;
        }

        if (value !== undefined && value !== null) {
          this.setUniform(name, value);
        }
      }
    }

    /**
     * Copies the shader from one drawing context to another.
     *
     * Each `p5.Shader` object must be compiled by calling
     * <a href="#/p5/shader">shader()</a> before it can run. Compilation happens
     * in a drawing context which is usually the main canvas or an instance of
     * <a href="#/p5.Graphics">p5.Graphics</a>. A shader can only be used in the
     * context where it was compiled. The `copyToContext()` method compiles the
     * shader again and copies it to another drawing context where it can be
     * reused.
     *
     * The parameter, `context`, is the drawing context where the shader will be
     * used. The shader can be copied to an instance of
     * <a href="#/p5.Graphics">p5.Graphics</a>, as in
     * `myShader.copyToContext(pg)`. The shader can also be copied from a
     * <a href="#/p5.Graphics">p5.Graphics</a> object to the main canvas using
     * the `p5.instance` variable, as in `myShader.copyToContext(p5.instance)`.
     *
     * Note: A <a href="#/p5.Shader">p5.Shader</a> object created with
     * <a href="#/p5/createShader">createShader()</a>,
     * <a href="#/p5/createFilterShader">createFilterShader()</a>, or
     * <a href="#/p5/loadShader">loadShader()</a>
     * can be used directly with a <a href="#/p5.Framebuffer">p5.Framebuffer</a>
     * object created with
     * <a href="#/p5/createFramebuffer">createFramebuffer()</a>. Both objects
     * have the same context as the main canvas.
     *
     * @param {p5|p5.Graphics} context WebGL context for the copied shader.
     * @returns {p5.Shader} new shader compiled for the target context.
     *
     * @example
     * // Note: A "uniform" is a global variable within a shader program.
     *
     * // Create a string with the vertex shader program.
     * // The vertex shader is called for each vertex.
     * let vertSrc = `
     * precision highp float;
     * uniform mat4 uModelViewMatrix;
     * uniform mat4 uProjectionMatrix;
     *
     * attribute vec3 aPosition;
     * attribute vec2 aTexCoord;
     * varying vec2 vTexCoord;
     *
     * void main() {
     *   vTexCoord = aTexCoord;
     *   vec4 positionVec4 = vec4(aPosition, 1.0);
     *   gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
     * }
     * `;
     *
     * // Create a string with the fragment shader program.
     * // The fragment shader is called for each pixel.
     * let fragSrc = `
     * precision mediump float;
     * varying vec2 vTexCoord;
     *
     * void main() {
     *   vec2 uv = vTexCoord;
     *   vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0));
     *   gl_FragColor = vec4(color, 1.0);\
     * }
     * `;
     *
     * let pg;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   // Create a p5.Shader object.
     *   let original = createShader(vertSrc, fragSrc);
     *
     *   // Compile the p5.Shader object.
     *   shader(original);
     *
     *   // Create a p5.Graphics object.
     *   pg = createGraphics(50, 50, WEBGL);
     *
     *   // Copy the original shader to the p5.Graphics object.
     *   let copied = original.copyToContext(pg);
     *
     *   // Apply the copied shader to the p5.Graphics object.
     *   pg.shader(copied);
     *
     *   // Style the display surface.
     *   pg.noStroke();
     *
     *   // Add a display surface for the shader.
     *   pg.plane(50, 50);
     *
     *   describe('A square with purple-blue gradient on its surface drawn against a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw the p5.Graphics object to the main canvas.
     *   image(pg, -25, -25);
     * }
     *
     * @example
     * // Note: A "uniform" is a global variable within a shader program.
     *
     * // Create a string with the vertex shader program.
     * // The vertex shader is called for each vertex.
     * let vertSrc = `
     * precision highp float;
     * uniform mat4 uModelViewMatrix;
     * uniform mat4 uProjectionMatrix;
     *
     * attribute vec3 aPosition;
     * attribute vec2 aTexCoord;
     * varying vec2 vTexCoord;
     *
     * void main() {
     *   vTexCoord = aTexCoord;
     *   vec4 positionVec4 = vec4(aPosition, 1.0);
     *   gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
     * }
     * `;
     *
     * // Create a string with the fragment shader program.
     * // The fragment shader is called for each pixel.
     * let fragSrc = `
     * precision mediump float;
     *
     * varying vec2 vTexCoord;
     *
     * void main() {
     *   vec2 uv = vTexCoord;
     *   vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0));
     *   gl_FragColor = vec4(color, 1.0);
     * }
     * `;
     *
     * let copied;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Graphics object.
     *   let pg = createGraphics(25, 25, WEBGL);
     *
     *   // Create a p5.Shader object.
     *   let original = pg.createShader(vertSrc, fragSrc);
     *
     *   // Compile the p5.Shader object.
     *   pg.shader(original);
     *
     *   // Copy the original shader to the main canvas.
     *   copied = original.copyToContext(p5.instance);
     *
     *   // Apply the copied shader to the main canvas.
     *   shader(copied);
     *
     *   describe('A rotating cube with a purple-blue gradient on its surface drawn against a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Rotate around the x-, y-, and z-axes.
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.01);
     *   rotateZ(frameCount * 0.01);
     *
     *   // Draw the box.
     *   box(50);
     * }
     */
    copyToContext(context) {
      const shader = new Shader(
        context._renderer,
        this._vertSrc,
        this._fragSrc
      );
      shader.ensureCompiledOnContext(context._renderer);
      return shader;
    }

    /**
     * @private
     */
    ensureCompiledOnContext(context) {
      if (this._glProgram !== 0 && this._renderer !== context) {
        throw new Error(
          'The shader being run is attached to a different context. Do you need to copy it to this context first with .copyToContext()?'
        );
      } else if (this._glProgram === 0) {
        this._renderer = context?._renderer?.filterRenderer?._renderer || context;
        this.init();
      }
    }

    /**
     * Queries the active attributes for this shader and loads
     * their names and locations into the attributes array.
     * @private
     */
    _loadAttributes() {
      if (this._loadedAttributes) {
        return;
      }
      this.attributes = this._renderer._getShaderAttributes(this);
      this._loadedAttributes = true;
    }

    /**
     * Queries the active uniforms for this shader and loads
     * their names and locations into the uniforms array.
     * @private
     */
    _loadUniforms() {
      if (this._loadedUniforms) {
        return;
      }

      this.uniforms = {};
      this.samplers = [];

      const uniformMetadata = this._renderer.getUniformMetadata(this);

      for (const meta of uniformMetadata) {
        const uniform = {
          ...meta,
          _cachedData: undefined,
        };

        if (uniform.isSampler) {
          this.samplers.push(uniform);
        }

        this.uniforms[uniform.name] = uniform;
      }

      this._loadedUniforms = true;
    }

    /**
     * initializes (if needed) and binds the shader program.
     * @private
     */
    bindShader(shaderType, options) {
      if (this.shaderType && this.shaderType !== shaderType) {
        throw new Error(
          `You've already used this shader as a ${this.shaderType} shader, but are now using it as a ${shaderType}.`
        );
      }
      this.shaderType = shaderType;
      this.init();
      if (!this._bound) {
        this.useProgram(options);
        this._bound = true;
      }
    }

    /**
     * @chainable
     * @private
     */
    unbindShader() {
      if (this._bound) {
        this.unbindTextures();
        this._bound = false;
      }
      return this;
    }

    /**
     * @private
     */
    bindTextures() {
      const empty = this._renderer._getEmptyTexture();

      for (const uniform of this.samplers) {
        if (uniform.noData) continue;
        let tex = uniform.texture;
        if (
          tex === undefined ||
          (
            // Make sure we unbind a framebuffer uniform if it's the same
            // framebuffer that is actvely being drawn to in order to
            // prevent a feedback cycle
            tex.isFramebufferTexture &&
            !tex.src.framebuffer.antialias &&
            tex.src.framebuffer === this._renderer.activeFramebuffer()
          )
        ) {
          // user hasn't yet supplied a texture for this slot.
          // (or there may not be one--maybe just lighting),
          // so we supply a default texture instead.
          uniform.texture = tex = empty;
        }
        this._renderer._updateTexture(uniform, tex);
      }
    }

    /**
     * @private
     */
    unbindTextures() {
      for (const uniform of this.samplers) {
        if (uniform.texture?.isFramebufferTexture) {
          this._renderer._unbindFramebufferTexture(uniform);
        }
      }
    }

    /**
     * @chainable
     * @private
     */
    useProgram(options) {
      if (this._renderer._curShader !== this) {
        this._renderer._useShader(this);
        this._renderer._curShader = this;
      }
      return this;
    }

    /**
     * Sets the shader’s uniform (global) variables.
     *
     * Shader programs run on the computer’s graphics processing unit (GPU).
     * They live in part of the computer’s memory that’s completely separate
     * from the sketch that runs them. Uniforms are global variables within a
     * shader program. They provide a way to pass values from a sketch running
     * on the CPU to a shader program running on the GPU.
     *
     * The first parameter, `uniformName`, is a string with the uniform’s name.
     * For the shader above, `uniformName` would be `'r'`.
     *
     * The second parameter, `data`, is the value that should be used to set the
     * uniform. For example, calling `myShader.setUniform('r', 0.5)` would set
     * the `r` uniform in the shader above to `0.5`. data should match the
     * uniform’s type. Numbers, strings, booleans, arrays, and many types of
     * images can all be passed to a shader with `setUniform()`.
     *
     * @chainable
     * @param {String} uniformName name of the uniform. Must match the name
     *                             used in the vertex and fragment shaders.
     * @param {Boolean|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture}
     * data value to assign to the uniform. Must match the uniform’s data type.
     *
     * @example
     * // Note: A "uniform" is a global variable within a shader program.
     *
     * // Create a string with the vertex shader program.
     * // The vertex shader is called for each vertex.
     * let vertSrc = `
     * precision highp float;
     * uniform mat4 uModelViewMatrix;
     * uniform mat4 uProjectionMatrix;
     *
     * attribute vec3 aPosition;
     * attribute vec2 aTexCoord;
     * varying vec2 vTexCoord;
     *
     * void main() {
     *   vTexCoord = aTexCoord;
     *   vec4 positionVec4 = vec4(aPosition, 1.0);
     *   gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
     * }
     * `;
     *
     * // Create a string with the fragment shader program.
     * // The fragment shader is called for each pixel.
     * let fragSrc = `
     * precision mediump float;
     *
     * uniform float r;
     *
     * void main() {
     *   gl_FragColor = vec4(r, 1.0, 1.0, 1.0);
     * }
     * `;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Shader object.
     *   let myShader = createShader(vertSrc, fragSrc);
     *
     *   // Apply the p5.Shader object.
     *   shader(myShader);
     *
     *   // Set the r uniform to 0.5.
     *   myShader.setUniform('r', 0.5);
     *
     *   // Style the drawing surface.
     *   noStroke();
     *
     *   // Add a plane as a drawing surface for the shader.
     *   plane(100, 100);
     *
     *   describe('A cyan square.');
     * }
     *
     * @example
     * // Note: A "uniform" is a global variable within a shader program.
     *
     * // Create a string with the vertex shader program.
     * // The vertex shader is called for each vertex.
     * let vertSrc = `
     * precision highp float;
     * uniform mat4 uModelViewMatrix;
     * uniform mat4 uProjectionMatrix;
     *
     * attribute vec3 aPosition;
     * attribute vec2 aTexCoord;
     * varying vec2 vTexCoord;
     *
     * void main() {
     *   vTexCoord = aTexCoord;
     *   vec4 positionVec4 = vec4(aPosition, 1.0);
     *   gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
     * }
     * `;
     *
     * // Create a string with the fragment shader program.
     * // The fragment shader is called for each pixel.
     * let fragSrc = `
     * precision mediump float;
     *
     * uniform float r;
     *
     * void main() {
     *   gl_FragColor = vec4(r, 1.0, 1.0, 1.0);
     * }
     * `;
     *
     * let myShader;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Shader object.
     *   myShader = createShader(vertSrc, fragSrc);
     *
     *   // Compile and apply the p5.Shader object.
     *   shader(myShader);
     *
     *   describe('A square oscillates color between cyan and white.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the drawing surface.
     *   noStroke();
     *
     *   // Update the r uniform.
     *   let nextR = 0.5 * (sin(frameCount * 0.01) + 1);
     *   myShader.setUniform('r', nextR);
     *
     *   // Add a plane as a drawing surface.
     *   plane(100, 100);
     * }
     *
     * @example
     * // Note: A "uniform" is a global variable within a shader program.
     *
     * // Create a string with the vertex shader program.
     * // The vertex shader is called for each vertex.
     * let vertSrc = `
     * precision highp float;
     * uniform mat4 uModelViewMatrix;
     * uniform mat4 uProjectionMatrix;
     *
     * attribute vec3 aPosition;
     * attribute vec2 aTexCoord;
     * varying vec2 vTexCoord;
     *
     * void main() {
     *   vTexCoord = aTexCoord;
     *   vec4 positionVec4 = vec4(aPosition, 1.0);
     *   gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
     * }
     * `;
     *
     * // Create a string with the fragment shader program.
     * // The fragment shader is called for each pixel.
     * let fragSrc = `
     * precision highp float;
     * uniform vec2 p;
     * uniform float r;
     * const int numIterations = 500;
     * varying vec2 vTexCoord;
     *
     * void main() {
     *   vec2 c = p + gl_FragCoord.xy * r;
     *   vec2 z = c;
     *   float n = 0.0;
     *
     *   for (int i = numIterations; i > 0; i--) {
     *     if (z.x * z.x + z.y * z.y > 4.0) {
     *       n = float(i) / float(numIterations);
     *       break;
     *     }
     *
     *     z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c;
     *   }
     *
     *   gl_FragColor = vec4(
     *     0.5 - cos(n * 17.0) / 2.0,
     *     0.5 - cos(n * 13.0) / 2.0,
     *     0.5 - cos(n * 23.0) / 2.0,
     *     1.0
     *   );
     * }
     * `;
     *
     * let mandelbrot;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Shader object.
     *   mandelbrot = createShader(vertSrc, fragSrc);
     *
     *   // Compile and apply the p5.Shader object.
     *   shader(mandelbrot);
     *
     *   // Set the shader uniform p to an array.
     *   // p is the center point of the Mandelbrot image.
     *   mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]);
     *
     *   describe('A fractal image zooms in and out of focus.');
     * }
     *
     * function draw() {
     *   // Set the shader uniform r to a value that oscillates
     *   // between 0 and 0.005.
     *   // r is the size of the image in Mandelbrot-space.
     *   let radius = 0.005 * (sin(frameCount * 0.01) + 1);
     *   mandelbrot.setUniform('r', radius);
     *
     *   // Style the drawing surface.
     *   noStroke();
     *
     *   // Add a plane as a drawing surface.
     *   plane(100, 100);
     * }
     */
    setUniform(uniformName, data) {
      this.init();

      const uniform = this.uniforms[uniformName];
      if (!uniform) {
        return;
      }

      if (uniform.isArray) {
        if (
          uniform._cachedData &&
          this._renderer._arraysEqual(uniform._cachedData, data)
        ) {
          return;
        } else {
          uniform._cachedData = data.slice(0);
        }
      } else if (uniform._cachedData && uniform._cachedData === data) {
        return;
      } else {
        if (Array.isArray(data) || data instanceof TypedArray) {
          if (uniform._cachedData && this._renderer._arraysEqual(uniform._cachedData, data)) {
            return;
          }
          uniform._cachedData = data.slice(0);
        } else {
          if (uniform._cachedData === data) return;
          uniform._cachedData = data;
        }
      }

      this._renderer.updateUniformValue(this, uniform, data);
    }

    /**
     * @chainable
     * @private
     */
    enableAttrib(attr, size, type, normalized, stride, offset) {
      if (attr) {
        if (
          typeof IS_MINIFIED === 'undefined' &&
          this.attributes[attr.name] !== attr
        ) {
          console.warn(
            `The attribute "${attr.name}"passed to enableAttrib does not belong to this shader.`
          );
        }

        if (attr.location !== -1) {
          this._renderer._enableAttrib(this, attr, size, type, normalized, stride, offset);
        }
      }
      return this;
    }
  }
  function shader(p5, fn){
    /**
     * A class to describe a shader program.
     *
     * Each `p5.Shader` object contains a shader program that runs on the graphics
     * processing unit (GPU). Shaders can process many pixels or vertices at the
     * same time, making them fast for many graphics tasks. They’re written in a
     * language called
     * <a href="https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_on_the_web/GLSL_Shaders" target="_blank">GLSL</a>
     * and run along with the rest of the code in a sketch.
     *
     * A shader program consists of two files, a vertex shader and a fragment
     * shader. The vertex shader affects where 3D geometry is drawn on the screen
     * and the fragment shader affects color. Once the `p5.Shader` object is
     * created, it can be used with the <a href="#/p5/shader">shader()</a>
     * function, as in `shader(myShader)`.
     *
     * A shader can optionally describe *hooks,* which are functions in GLSL that
     * users may choose to provide to customize the behavior of the shader. For the
     * vertex or the fragment shader, users can pass in an object where each key is
     * the type and name of a hook function, and each value is a string with the
     * parameter list and default implementation of the hook. For example, to let users
     * optionally run code at the start of the vertex shader, the options object could
     * include:
     *
     * ```js
     * {
     *   vertex: {
     *     'void beforeVertex': '() {}'
     *   }
     * }
     * ```
     *
     * Then, in your vertex shader source, you can run a hook by calling a function
     * with the same name prefixed by `HOOK_`:
     *
     * ```glsl
     * void main() {
     *   HOOK_beforeVertex();
     *   // Add the rest ofy our shader code here!
     * }
     * ```
     *
     * Note: <a href="#/p5/createShader">createShader()</a>,
     * <a href="#/p5/createFilterShader">createFilterShader()</a>, and
     * <a href="#/p5/loadShader">loadShader()</a> are the recommended ways to
     * create an instance of this class.
     *
     * @class p5.Shader
     * @constructor
     * @param {p5.RendererGL} renderer WebGL context for this shader.
     * @param {String} vertSrc source code for the vertex shader program.
     * @param {String} fragSrc source code for the fragment shader program.
     * @param {Object} [options] An optional object describing how this shader can
     * be augmented with hooks. It can include:
     *  - `vertex`: An object describing the available vertex shader hooks.
     *  - `fragment`: An object describing the available frament shader hooks.
     *
     * @example
     * // Note: A "uniform" is a global variable within a shader program.
     *
     * // Create a string with the vertex shader program.
     * // The vertex shader is called for each vertex.
     * let vertSrc = `
     * precision highp float;
     * uniform mat4 uModelViewMatrix;
     * uniform mat4 uProjectionMatrix;
     *
     * attribute vec3 aPosition;
     * attribute vec2 aTexCoord;
     * varying vec2 vTexCoord;
     *
     * void main() {
     *   vTexCoord = aTexCoord;
     *   vec4 positionVec4 = vec4(aPosition, 1.0);
     *   gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
     * }
     * `;
     *
     * // Create a string with the fragment shader program.
     * // The fragment shader is called for each pixel.
     * let fragSrc = `
     * precision highp float;
     *
     * void main() {
     *   // Set each pixel's RGBA value to yellow.
     *   gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
     * }
     * `;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Shader object.
     *   let myShader = createShader(vertSrc, fragSrc);
     *
     *   // Apply the p5.Shader object.
     *   shader(myShader);
     *
     *   // Style the drawing surface.
     *   noStroke();
     *
     *   // Add a plane as a drawing surface.
     *   plane(100, 100);
     *
     *   describe('A yellow square.');
     * }
     *
     * @example
     * // Note: A "uniform" is a global variable within a shader program.
     *
     * let mandelbrot;
     *
     * async function setup() {
     *   mandelbrot = await loadShader('assets/shader.vert', 'assets/shader.frag');
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Use the p5.Shader object.
     *   shader(mandelbrot);
     *
     *   // Set the shader uniform p to an array.
     *   mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]);
     *
     *   describe('A fractal image zooms in and out of focus.');
     * }
     *
     * function draw() {
     *   // Set the shader uniform r to a value that oscillates between 0 and 2.
     *   mandelbrot.setUniform('r', sin(frameCount * 0.01) + 1);
     *
     *   // Add a quad as a display surface for the shader.
     *   quad(-1, -1, 1, -1, 1, 1, -1, 1);
     * }
     */
    p5.Shader = Shader;
  }

  if(typeof p5 !== 'undefined'){
    shader(p5);
  }

  /**
   * @module 3D
   * @submodule Material
   * @for p5
   * @requires core
   */


  async function urlToStrandsCallback(url) {
    const src = await fetch(url).then((res) => res.text());
    return new Function(src);
  }

  function withGlobalStrands(p5, cb) {
    const prevGlobalStrands = p5._runStrandsInGlobalMode;
    p5._runStrandsInGlobalMode = true;
    try {
      return cb();
    } finally {
      p5._runStrandsInGlobalMode = prevGlobalStrands;
    }
  }

  function material(p5, fn) {
    /**
     * Loads vertex and fragment shaders to create a
     * <a href="#/p5.Shader">p5.Shader</a> object.
     *
     * Shaders are programs that run on the graphics processing unit (GPU). They
     * can process many pixels at the same time, making them fast for many
     * graphics tasks. They’re written in a language called
     * <a href="https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_on_the_web/GLSL_Shaders" target="_blank">GLSL</a>
     * and run along with the rest of the code in a sketch.
     *
     * Once the <a href="#/p5.Shader">p5.Shader</a> object is created, it can be
     * used with the <a href="#/p5/shader">shader()</a> function, as in
     * `shader(myShader)`. A shader program consists of two files, a vertex shader
     * and a fragment shader. The vertex shader affects where 3D geometry is drawn
     * on the screen and the fragment shader affects color.
     *
     * `loadShader()` loads the vertex and fragment shaders from their `.vert` and
     * `.frag` files. For example, calling
     * `loadShader('assets/shader.vert', 'assets/shader.frag')` loads both
     * required shaders and returns a <a href="#/p5.Shader">p5.Shader</a> object.
     *
     * The third parameter, `successCallback`, is optional. If a function is
     * passed, it will be called once the shader has loaded. The callback function
     * can use the new <a href="#/p5.Shader">p5.Shader</a> object as its
     * parameter. The return value of the `successCallback()` function will be used
     * as the final return value of `loadShader()`.
     *
     * The fourth parameter, `failureCallback`, is also optional. If a function is
     * passed, it will be called if the shader fails to load. The callback
     * function can use the event error as its parameter. The return value of the `
     * failureCallback()` function will be used as the final return value of `loadShader()`.
     *
     * This function returns a `Promise` and should be used in an `async` setup with
     * `await`. See the examples for the usage syntax.
     *
     * Note: Shaders can only be used in WebGL mode.
     *
     * @method loadShader
     * @param {String|Request} vertFilename path of the vertex shader to be loaded.
     * @param {String|Request} fragFilename path of the fragment shader to be loaded.
     * @param {Function} [successCallback] function to call once the shader is loaded. Can be passed the
     *                                     <a href="#/p5.Shader">p5.Shader</a> object.
     * @param {Function} [failureCallback] function to call if the shader fails to load. Can be passed an
     *                                     `Error` event object.
     * @return {Promise<p5.Shader>} new shader created from the vertex and fragment shader files.
     *
     * @example
     * // Note: A "uniform" is a global variable within a shader program.
     *
     * let mandelbrot;
     *
     * // Load the shader and create a p5.Shader object.
     * async function setup() {
     *   mandelbrot = await loadShader('assets/shader.vert', 'assets/shader.frag');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Compile and apply the p5.Shader object.
     *   shader(mandelbrot);
     *
     *   // Set the shader uniform p to an array.
     *   mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]);
     *
     *   // Set the shader uniform r to the value 1.5.
     *   mandelbrot.setUniform('r', 1.5);
     *
     *   // Add a quad as a display surface for the shader.
     *   quad(-1, -1, 1, -1, 1, 1, -1, 1);
     *
     *   describe('A black fractal image on a magenta background.');
     * }
     *
     * @example
     * // Note: A "uniform" is a global variable within a shader program.
     *
     * let mandelbrot;
     *
     * // Load the shader and create a p5.Shader object.
     * async function setup() {
     *   mandelbrot = await loadShader('assets/shader.vert', 'assets/shader.frag');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Use the p5.Shader object.
     *   shader(mandelbrot);
     *
     *   // Set the shader uniform p to an array.
     *   mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]);
     *
     *   describe('A fractal image zooms in and out of focus.');
     * }
     *
     * function draw() {
     *   // Set the shader uniform r to a value that oscillates between 0 and 2.
     *   mandelbrot.setUniform('r', sin(frameCount * 0.01) + 1);
     *
     *   // Add a quad as a display surface for the shader.
     *   quad(-1, -1, 1, -1, 1, 1, -1, 1);
     * }
     */
    fn.loadShader = async function (
      vertFilename,
      fragFilename,
      successCallback,
      failureCallback,
    ) {
      // p5._validateParameters('loadShader', arguments);

      const loadedShader = new Shader();

      try {
        loadedShader._vertSrc = (await request(vertFilename, "text")).data;
        loadedShader._fragSrc = (await request(fragFilename, "text")).data;

        if (successCallback) {
          return successCallback(loadedShader) || loadedShader;
        } else {
          return loadedShader;
        }
      } catch (err) {
        if (failureCallback) {
          return failureCallback(err);
        } else {
          throw err;
        }
      }
    };

    /**
     * Creates a new <a href="#/p5.Shader">p5.Shader</a> object using GLSL.
     *
     * If you are interested in writing shaders, consider using p5.strands shaders using
     * <a href="#/p5/buildMaterialShader">`buildMaterialShader`<a>,
     * <a href="#/p5/buildStrokeShader">`buildStrokeShader`</a>, or
     * <a href="#/p5/buildFilterShader">`buildFilterShader`</a>.
     * With p5.strands, you can modify existing shaders using JavaScript. With
     * `createShader`, shaders are made from scratch, and are written in GLSL. This
     * will be most useful for advanced cases, and for authors of add-on libraries.
     *
     * Shaders are programs that run on the graphics processing unit (GPU). They
     * can process many pixels at the same time, making them fast for many
     * graphics tasks.
     *
     * Once the <a href="#/p5.Shader">p5.Shader</a> object is created, it can be
     * used with the <a href="#/p5/shader">shader()</a> function, as in
     * `shader(myShader)`. A GLSL shader program consists of two parts, a vertex shader
     * and a fragment shader. The vertex shader affects where 3D geometry is drawn
     * on the screen and the fragment shader affects color.
     *
     * The first parameter, `vertSrc`, sets the vertex shader. It’s a string that
     * contains the vertex shader program written in GLSL.
     *
     * The second parameter, `fragSrc`, sets the fragment shader. It’s a string
     * that contains the fragment shader program written in GLSL.
     *
     * Here is a simple example with a simple vertex shader that applies whatevre
     * transformations have been set, and a simple fragment shader that ignores
     * all material settings and just outputs yellow:
     *
     * ```js example
     * // Create a string with the vertex shader program.
     * // The vertex shader is called for each vertex.
     * let vertSrc = `
     * precision highp float;
     * uniform mat4 uModelViewMatrix;
     * uniform mat4 uProjectionMatrix;
     * attribute vec3 aPosition;
     * attribute vec2 aTexCoord;
     * varying vec2 vTexCoord;
     *
     * void main() {
     *   vTexCoord = aTexCoord;
     *   vec4 positionVec4 = vec4(aPosition, 1.0);
     *   gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
     * }
     * `;
     *
     * // Create a string with the fragment shader program.
     * // The fragment shader is called for each pixel.
     * let fragSrc = `
     * precision highp float;
     *
     * void main() {
     *   // Set each pixel's RGBA value to yellow.
     *   gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
     * }
     * `;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Shader object.
     *   let shaderProgram = createShader(vertSrc, fragSrc);
     *
     *   // Compile and apply the p5.Shader object.
     *   shader(shaderProgram);
     *
     *   // Style the drawing surface.
     *   noStroke();
     *
     *   // Add a plane as a drawing surface.
     *   plane(100, 100);
     *
     *   describe('A yellow square.');
     * }
     * ```
     *
     * Fragment shaders are often the fastest way to dynamically create per-pixel textures.
     * Here is an example of a fractal being drawn in the fragment shader. It also creates custom
     * *uniform* variables in the shader, which can be set from your main sketch code. By passing
     * the time in as a uniform, we can animate the fractal in the shader.
     *
     * ```js example
     * // Create a string with the vertex shader program.
     * // The vertex shader is called for each vertex.
     * let vertSrc = `
     * precision highp float;
     * uniform mat4 uModelViewMatrix;
     * uniform mat4 uProjectionMatrix;
     *
     * attribute vec3 aPosition;
     * attribute vec2 aTexCoord;
     * varying vec2 vTexCoord;
     *
     * void main() {
     *   vTexCoord = aTexCoord;
     *   vec4 positionVec4 = vec4(aPosition, 1.0);
     *   gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
     * }
     * `;
     *
     * // Create a string with the fragment shader program.
     * // The fragment shader is called for each pixel.
     * let fragSrc = `
     * precision highp float;
     * uniform vec2 p;
     * uniform float r;
     * const int numIterations = 500;
     * varying vec2 vTexCoord;
     *
     * void main() {
     *   vec2 c = p + gl_FragCoord.xy * r;
     *   vec2 z = c;
     *   float n = 0.0;
     *
     *   for (int i = numIterations; i > 0; i--) {
     *     if (z.x * z.x + z.y * z.y > 4.0) {
     *       n = float(i) / float(numIterations);
     *       break;
     *     }
     *
     *     z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c;
     *   }
     *
     *   gl_FragColor = vec4(
     *     0.5 - cos(n * 17.0) / 2.0,
     *     0.5 - cos(n * 13.0) / 2.0,
     *     0.5 - cos(n * 23.0) / 2.0,
     *     1.0
     *   );
     * }
     * `;
     *
     * let mandelbrot;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Shader object.
     *   mandelbrot = createShader(vertSrc, fragSrc);
     *
     *   // Apply the p5.Shader object.
     *   shader(mandelbrot);
     *
     *   // Set the shader uniform p to an array.
     *   // p is the center point of the Mandelbrot image.
     *   mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]);
     *
     *   describe('A fractal image zooms in and out of focus.');
     * }
     *
     * function draw() {
     *   // Set the shader uniform r to a value that oscillates
     *   // between 0 and 0.005.
     *   // r is the size of the image in Mandelbrot-space.
     *   let radius = 0.005 * (sin(frameCount * 0.01) + 1);
     *   mandelbrot.setUniform('r', radius);
     *
     *   // Add a plane as a drawing surface.
     *   noStroke();
     *   plane(100, 100);
     * }
     * ```
     *
     * A shader can optionally describe *hooks,* which are functions in GLSL that
     * users may choose to provide to customize the behavior of the shader using the
     * <a href="#/p5.Shader/modify">`modify()`</a> method of `p5.Shader`. Users can
     * write their modifications using p5.strands, without needing to learn GLSL.
     *
     * These are added by
     * describing the hooks in a third parameter, `options`, and referencing the hooks in
     * your `vertSrc` or `fragSrc`. Hooks for the vertex or fragment shader are described under
     * the `vertex` and `fragment` keys of `options`. Each one is an object. where each key is
     * the type and name of a hook function, and each value is a string with the
     * parameter list and default implementation of the hook. For example, to let users
     * optionally run code at the start of the vertex shader, the options object could
     * include:
     *
     * ```js
     * {
     *   vertex: {
     *     'void beforeVertex': '() {}'
     *   }
     * }
     * ```
     *
     * Then, in your vertex shader source, you can run a hook by calling a function
     * with the same name prefixed by `HOOK_`. If you want to check if the default
     * hook has been replaced, maybe to avoid extra overhead, you can check if the
     * same name prefixed by `AUGMENTED_HOOK_` has been defined:
     *
     * ```glsl
     * void main() {
     *   // In most cases, just calling the hook is fine:
     *   HOOK_beforeVertex();
     *
     *   // Alternatively, for more efficiency:
     *   #ifdef AUGMENTED_HOOK_beforeVertex
     *   HOOK_beforeVertex();
     *   #endif
     *
     *   // Add the rest of your shader code here!
     * }
     * ```
     *
     * Then, a user of your shader can modify it with p5.strands. Here is what
     * that looks like when we put everything together:
     *
     * ```js example
     * // A shader with hooks.
     * let myShader;
     *
     * // A shader with modified hooks.
     * let modifiedShader;
     *
     * // Create a string with the vertex shader program.
     * // The vertex shader is called for each vertex.
     * let vertSrc = `
     * precision highp float;
     * uniform mat4 uModelViewMatrix;
     * uniform mat4 uProjectionMatrix;
     *
     * attribute vec3 aPosition;
     * attribute vec2 aTexCoord;
     *
     * void main() {
     *   vec4 positionVec4 = vec4(aPosition, 1.0);
     *   gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
     * }
     * `;
     *
     * // Create a fragment shader that uses a hook.
     * let fragSrc = `
     * precision highp float;
     * void main() {
     *   // Let users override the color
     *   gl_FragColor = HOOK_getColor(vec4(1., 0., 0., 1.));
     * }
     * `;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a shader with hooks. By default, this hook returns
     *   // the initial value.
     *   myShader = createShader(vertSrc, fragSrc, {
     *     fragment: {
     *       'vec4 getColor': '(vec4 color) { return color; }'
     *     }
     *   });
     *
     *   // Make a version of the shader with a hook overridden
     *   modifiedShader = myShader.modify(() => {
     *     // Create new uniforms and override the getColor hook
     *     let t = uniformFloat(() => millis() / 1000);
     *     getColor(() => {
     *       return [0, 0.5 + 0.5 * sin(t), 1, 1];
     *     });
     *   });
     * }
     *
     * function draw() {
     *   noStroke();
     *
     *   push();
     *   shader(myShader);
     *   translate(-width/3, 0);
     *   sphere(20);
     *   pop();
     *
     *   push();
     *   shader(modifiedShader);
     *   translate(width/3, 0);
     *   sphere(20);
     *   pop();
     * }
     * ```
     *
     * Note: Only filter shaders can be used in 2D mode. All shaders can be used
     * in WebGL mode.
     *
     * @method createShader
     * @param {String} vertSrc source code for the vertex shader.
     * @param {String} fragSrc source code for the fragment shader.
     * @param {Object} [options] An optional object describing how this shader can
     * be augmented with hooks. It can include:
     * @param {Object} [options.vertex] An object describing the available vertex shader hooks.
     * @param {Object} [options.fragment] An object describing the available frament shader hooks.
     * @returns {p5.Shader} new shader object created from the
     * vertex and fragment shaders.
     */
    fn.createShader = function (vertSrc, fragSrc, options) {
      // p5._validateParameters('createShader', arguments);
      return new Shader(this._renderer, vertSrc, fragSrc, options);
    };

    /**
     * Loads a new shader from a file that can be applied to the contents of the canvas with
     * <a href="#/p5/filter">`filter()`</a>. Pass the resulting shader into `filter()` to apply it.
     *
     * Since this function loads data from another file, it returns a `Promise`.
     * Use it in an `async function setup`, and `await` its result.
     *
     * ```js
     * async function setup() {
     *   createCanvas(50, 50, WEBGL);
     *   let img = await loadImage('assets/bricks.jpg');
     *   let myFilter = await loadFilterShader('myFilter.js');
     *
     *   image(img, -50, -50);
     *   filter(myFilter);
     *   describe('Bricks tinted red');
     * }
     * ```
     *
     * Inside your shader file, you can use p5.strands hooks to change parts of the shader. For
     * a filter shader, use `filterColor` to change each pixel on the canvas.
     *
     * ```js
     * // myFilter.js
     * filterColor.begin();
     * let result = getTexture(
     *   filterColor.canvasContent,
     *   filterColor.texCoord
     * );
     * // Zero out the green and blue channels, leaving red
     * result.g = 0;
     * result.b = 0;
     * filterColor.set(result);
     * filterColor.end();
     * ```
     *
     * Read the reference for <a href="#/p5/buildFilterShader">`buildFilterShader`</a>,
     * the version of `loadFilterShader` that takes in a function instead of a separate file,
     * for more examples.
     *
     * The second parameter, `successCallback`, is optional. If a function is passed, as in
     * `loadFilterShader('myShader.js', onLoaded)`, then the `onLoaded()` function will be called
     * once the shader loads. The shader will be passed to `onLoaded()` as its only argument.
     * The return value of `handleData()`, if present, will be used as the final return value of
     * `loadFilterShader('myShader.js', onLoaded)`.
     *
     * @method loadFilterShader
     * @submodule p5.strands
     * @param {String} filename path to a p5.strands JavaScript file or a GLSL fragment shader file
     * @param {Function} [successCallback] callback to be called once the shader is
     *                                     loaded. Will be passed the
     *                                     <a href="#/p5.Shader">p5.Shader</a> object.
     * @param {Function} [failureCallback] callback to be called if there is an error
     *                                     loading the shader. Will be passed the
     *                                     error event.
     * @return {Promise<p5.Shader>} a promise that resolves with a shader object
     */
    fn.loadFilterShader = async function (
      fragFilename,
      successCallback,
      failureCallback,
    ) {
      // p5._validateParameters('loadFilterShader', arguments);
      try {
        // Load the fragment shader
        const fragSrc = await this.loadStrings(fragFilename);
        const fragString = await fragSrc.join("\n");

        // Test if we've loaded GLSL or not by checking for the existence of `void main`
        let loadedShader;
        if (/void\s+main/.exec(fragString)) {
          loadedShader = this.createFilterShader(fragString, true);
        } else {
          loadedShader = withGlobalStrands(this, () =>
            this.baseFilterShader().modify(new Function(fragString)),
          );
        }

        if (successCallback) {
          loadedShader = successCallback(loadedShader) || loadedShader;
        }

        return loadedShader;
      } catch (err) {
        if (failureCallback) {
          failureCallback(err);
        } else {
          console.error(err);
        }
      }
    };

    /**
     * Creates a <a href="#/p5.Shader">p5.Shader</a> object to be used with the
     * <a href="#/p5/filter">filter()</a> function.
     *
     * The main way to use `buildFilterShader` is to pass a function in as a parameter.
     * This will let you create a shader using p5.strands.
     *
     * In your function, you can use <a href="#/p5/filterColor">`filterColor`</a> with a function
     * that will be called for each pixel on the image to determine its final color. You can
     * read the color of the current pixel with `getTexture(canvasContent, coord)`.
     * See <a href="#/p5/getTexture">getTexture()</a>.
     *
     * ```js example
     * async function setup() {
     *   createCanvas(50, 50, WEBGL);
     *   let img = await loadImage('assets/bricks.jpg');
     *   let myFilter = buildFilterShader(tintShader);
     *
     *   image(img, -50, -50);
     *   filter(myFilter);
     *   describe('Bricks tinted red');
     * }
     *
     * function tintShader() {
     *   filterColor.begin();
     *   let result = getTexture(
     *     filterColor.canvasContent,
     *     filterColor.texCoord
     *   );
     *   // Zero out the green and blue channels, leaving red
     *   result.g = 0;
     *   result.b = 0;
     *   filterColor.set(result);
     *   filterColor.end();
     * }
     * ```
     *
     * You can create *uniforms* if you want to pass data into your filter from the rest of your sketch.
     * For example, you could pass in the mouse cursor position and use that to control how much
     * you warp the content. If you create a uniform inside the shader using a function like `uniformFloat()`, with
     * `uniform` + the type of the data, you can set its value using `setUniform` right before applying the filter.
     * In the example below, move your mouse across the image to see it update the `warpAmount` uniform:
     *
     * ```js example
     * let img;
     * let myFilter;
     * async function setup() {
     *   createCanvas(50, 50, WEBGL);
     *   img = await loadImage('assets/bricks.jpg');
     *   myFilter = buildFilterShader(warpShader);
     *   describe('Warped bricks');
     * }
     *
     * function warpShader() {
     *   let warpAmount = uniformFloat();
     *   filterColor.begin();
     *   let coord = filterColor.texCoord;
     *   coord.y += sin(coord.x * 10) * warpAmount;
     *   filterColor.set(
     *     getTexture(filterColor.canvasContent, coord)
     *   );
     *   filterColor.end();
     * }
     *
     * function draw() {
     *   image(img, -50, -50);
     *   myFilter.setUniform(
     *     'warpAmount',
     *     map(mouseX, 0, width, 0, 1, true)
     *   );
     *   filter(myFilter);
     * }
     * ```
     *
     * You can also make filters that do not need any content to be drawn first!
     * There is a lot you can draw just using, for example, the position of the pixel.
     * `inputs.texCoord` has an `x` and a `y` property, each with a number between 0 and 1.
     *
     * ```js example
     * function setup() {
     *   createCanvas(50, 50, WEBGL);
     *   let myFilter = buildFilterShader(gradient);
     *   describe('A gradient with red, green, yellow, and black');
     *   filter(myFilter);
     * }
     *
     * function gradient() {
     *   filterColor.begin();
     *   filterColor.set([filterColor.texCoord.x, filterColor.texCoord.y, 0, 1]);
     *   filterColor.end();
     * }
     * ```
     *
     * ```js example
     * function setup() {
     *   createCanvas(50, 50, WEBGL);
     *   let myFilter = buildFilterShader(gradient);
     *   describe('A gradient from red to blue');
     *   filter(myFilter);
     * }
     *
     * function gradient() {
     *   filterColor.begin();
     *   filterColor.set(mix(
     *     [1, 0, 0, 1], // Red
     *     [0, 0, 1, 1], // Blue
     *     filterColor.texCoord.x // x coordinate, from 0 to 1
     *   ));
     *   filterColor.end();
     * }
     * ```
     *
     * You can also animate your filters over time by passing the time into the shader with `uniformFloat`.
     *
     * ```js example
     * let myFilter;
     * function setup() {
     *   createCanvas(50, 50, WEBGL);
     *   myFilter = buildFilterShader(gradient);
     *   describe('A moving, repeating gradient from red to blue');
     * }
     *
     * function gradient() {
     *   let time = uniformFloat();
     *   filterColor.begin();
     *   filterColor.set(mix(
     *     [1, 0, 0, 1], // Red
     *     [0, 0, 1, 1], // Blue
     *     sin(filterColor.texCoord.x*15 + time*0.004)/2+0.5
     *   ));
     *   filterColor.end();
     * }
     *
     * function draw() {
     *   myFilter.setUniform('time', millis());
     *   filter(myFilter);
     * }
     * ```
     *
     * Like the `modify()` method on shaders,
     * advanced users can also fill in `filterColor` using <a href="https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_on_the_web/GLSL_Shaders" target="_blank">GLSL</a>
     * instead of JavaScript.
     * Read the <a href="#/p5.Shader/modify">reference entry for `modify()`</a>
     * for more info. Alternatively, `buildFilterShader()` can also be used like
     * <a href="#/p5/createShader">createShader()</a>, but where you only specify a fragment shader.
     *
     * For more info about filters and shaders, see Adam Ferriss' <a href="https://github.com/aferriss/p5jsShaderExamples">repo of shader examples</a>
     * or the <a href="https://p5js.org/learn/getting-started-in-webgl-shaders.html">Introduction to Shaders</a> tutorial.
     *
     * @method buildFilterShader
     * @beta
     * @submodule p5.strands
     * @param {Function} callback A function building a p5.strands shader.
     * @returns {p5.Shader} The material shader
     */
    /**
     * @method buildFilterShader
     * @param {Object} hooks An object specifying p5.strands hooks in GLSL.
     * @returns {p5.Shader} The material shader
     */
    fn.buildFilterShader = function (callback) {
      return this.baseFilterShader().modify(callback);
    };

    /**
     * Creates a <a href="#/p5.Shader">p5.Shader</a> object to be used with the
     * <a href="#/p5/filter">filter()</a> function using GLSL.
     *
     * Since this method requires you to write your shaders in GLSL, it is most suitable
     * for advanced use cases. Consider using <a href="#/p5/buildFilterShader">`buildFilterShader`</a>
     * first, as a way to create filters in JavaScript using p5.strands.
     *
     * `createFilterShader()` works like
     * <a href="#/p5/createShader">createShader()</a> but has a default vertex
     * shader included. `createFilterShader()` is intended to be used along with
     * <a href="#/p5/filter">filter()</a> for filtering the contents of a canvas.
     * A filter shader will be applied to the whole canvas instead of just
     * <a href="#/p5.Geometry">p5.Geometry</a> objects.
     *
     * The parameter, `fragSrc`, sets the fragment shader. It’s a string that
     * contains the fragment shader program written in
     * <a href="https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_on_the_web/GLSL_Shaders" target="_blank">GLSL</a>.
     *
     * The <a href="#/p5.Shader">p5.Shader</a> object that's created has some
     * uniforms that can be set:
     * - `sampler2D tex0`, which contains the canvas contents as a texture.
     * - `vec2 canvasSize`, which is the width and height of the canvas, not including pixel density.
     * - `vec2 texelSize`, which is the size of a physical pixel including pixel density. This is calculated as `1.0 / (width * density)` for the pixel width and `1.0 / (height * density)` for the pixel height.
     *
     * The <a href="#/p5.Shader">p5.Shader</a> that's created also provides
     * `varying vec2 vTexCoord`, a coordinate with values between 0 and 1.
     * `vTexCoord` describes where on the canvas the pixel will be drawn.
     *
     * For more info about filters and shaders, see Adam Ferriss' <a href="https://github.com/aferriss/p5jsShaderExamples">repo of shader examples</a>
     * or the <a href="https://p5js.org/learn/getting-started-in-webgl-shaders.html">Introduction to Shaders</a> tutorial.
     *
     * @method createFilterShader
     * @param {String} fragSrc source code for the fragment shader.
     * @returns {p5.Shader} new shader object created from the fragment shader.
     *
     * @example
     * function setup() {
     *   let fragSrc = `precision highp float;
     *   void main() {
     *     gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
     *   }`;
     *
     *   createCanvas(100, 100, WEBGL);
     *   let s = createFilterShader(fragSrc);
     *   filter(s);
     *   describe('a yellow canvas');
     * }
     *
     * @example
     * let img, s;
     * async function setup() {
     *   img = await loadImage('assets/bricks.jpg');
     *   let fragSrc = `precision highp float;
     *
     *   // x,y coordinates, given from the vertex shader
     *   varying vec2 vTexCoord;
     *
     *   // the canvas contents, given from filter()
     *   uniform sampler2D tex0;
     *   // other useful information from the canvas
     *   uniform vec2 texelSize;
     *   uniform vec2 canvasSize;
     *   // a custom variable from this sketch
     *   uniform float darkness;
     *
     *   void main() {
     *     // get the color at current pixel
     *     vec4 color = texture2D(tex0, vTexCoord);
     *     // set the output color
     *     color.b = 1.0;
     *     color *= darkness;
     *     gl_FragColor = vec4(color.rgb, 1.0);
     *   }`;
     *
     *   createCanvas(100, 100, WEBGL);
     *   s = createFilterShader(fragSrc);
     * }
     *
     * function draw() {
     *   image(img, -50, -50);
     *   s.setUniform('darkness', 0.5);
     *   filter(s);
     *   describe('a image of bricks tinted dark blue');
     * }
     */
    fn.createFilterShader = function (fragSrc, skipContextCheck = false) {
      // p5._validateParameters('buildFilterShader', arguments);
      let defaultVertV1 = `
      uniform mat4 uModelViewMatrix;
      uniform mat4 uProjectionMatrix;

      attribute vec3 aPosition;
      // texcoords only come from p5 to vertex shader
      // so pass texcoords on to the fragment shader in a varying variable
      attribute vec2 aTexCoord;
      varying vec2 vTexCoord;

      void main() {
        // transferring texcoords for the frag shader
        vTexCoord = aTexCoord;

        // copy position with a fourth coordinate for projection (1.0 is normal)
        vec4 positionVec4 = vec4(aPosition, 1.0);

        // project to 3D space
        gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
      }
    `;
      let defaultVertV2 = `#version 300 es
      uniform mat4 uModelViewMatrix;
      uniform mat4 uProjectionMatrix;

      in vec3 aPosition;
      in vec2 aTexCoord;
      out vec2 vTexCoord;

      void main() {
        // transferring texcoords for the frag shader
        vTexCoord = aTexCoord;

        // copy position with a fourth coordinate for projection (1.0 is normal)
        vec4 positionVec4 = vec4(aPosition, 1.0);

        // project to 3D space
        gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
      }
    `;
      let vertSrc = fragSrc.includes("#version 300 es")
        ? defaultVertV2
        : defaultVertV1;
      const shader = new Shader(this._renderer, vertSrc, fragSrc);
      if (!skipContextCheck) {
        if (this._renderer.GL) {
          shader.ensureCompiledOnContext(this._renderer);
        } else {
          shader.ensureCompiledOnContext(this);
        }
      }
      return shader;
    };

    /**
     * Sets the <a href="#/p5.Shader">p5.Shader</a> object to apply while drawing.
     *
     * Shaders are programs that run on the graphics processing unit (GPU). They
     * can process many pixels or vertices at the same time, making them fast for
     * many graphics tasks.
     *
     * You can make new shaders using p5.strands with the
     * <a href="#/p5/buildMaterialShader">`buildMaterialShader`</a>,
     * <a href="#/p5/buildColorShader">`buildColorShader`</a>, and
     * <a href="#/p5/buildNormalShader">`buildNormalShader`</a> functions. You can also use
     * <a href="#/p5/buildFilterShader">`buildFilterShader`</a> alongside
     * <a href="#/p5/filter">`filter`</a>, and
     * <a href="#/p5/buildStrokeShader">`buildStrokeShader`</a> alongside
     * <a href="#/p5/stroke">`stroke`</a>.
     *
     * The parameter, `s`, is the <a href="#/p5.Shader">p5.Shader</a> object to
     * apply. For example, calling `shader(myShader)` applies `myShader` to
     * process each pixel on the canvas. This only changes the fill (the inner part of shapes),
     * but does not affect the outlines (strokes) or any images drawn using the `image()` function.
     * The source code from a <a href="#/p5.Shader">p5.Shader</a> object's
     * fragment and vertex shaders will be compiled the first time it's passed to
     * `shader()`.
     *
     * Calling <a href="#/p5/resetShader">resetShader()</a> restores a sketch’s
     * default shaders.
     *
     * Note: Shaders can only be used in WebGL mode.
     *
     * ```js example
     * let myShader;
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = buildMaterialShader(material);
     *   noStroke();
     *   describe('A square with dynamically changing colors on a beige background.');
     * }
     *
     * function material() {
     *   let time = uniformFloat();
     *   finalColor.begin();
     *   let r = 0.2 + 0.5 * abs(sin(time + 0));
     *   let g = 0.2 + 0.5 * abs(sin(time + 1));
     *   let b = 0.2 + 0.5 * abs(sin(time + 2));
     *   finalColor.set([r, g, b, 1]);
     *   finalColor.end();
     * }
     *
     * function draw() {
     *   background(245, 245, 220);
     *   myShader.setUniform('time', millis() / 1000);
     *   shader(myShader);
     *
     *   rectMode(CENTER);
     *   rect(0, 0, 50, 50);
     * }
     * ```
     *
     * For advanced usage, shaders can be written in a language called
     * <a href="https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_on_the_web/GLSL_Shaders" target="_blank">GLSL</a>.
     * <a href="#/p5.Shader">p5.Shader</a> objects can be created in this way using the
     * <a href="#/p5/createShader">createShader()</a> and
     * <a href="#/p5/loadShader">loadShader()</a> functions.
     *
     * ```js
     * let fillShader;
     *
     * let vertSrc = `
     * precision highp float;
     * attribute vec3 aPosition;
     * uniform mat4 uModelViewMatrix;
     * uniform mat4 uProjectionMatrix;
     * varying vec3 vPosition;
     *
     * void main() {
     *   vPosition = aPosition;
     *   gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0);
     * }
     * `;
     *
     * let fragSrc = `
     * precision highp float;
     * uniform vec3 uLightDir;
     * varying vec3 vPosition;
     *
     * void main() {
     *   vec3 lightDir = normalize(uLightDir);
     *   float brightness = dot(lightDir, normalize(vPosition));
     *   brightness = clamp(brightness, 0.4, 1.0);
     *   vec3 color = vec3(0.3, 0.5, 1.0);
     *   color = color * brightness * 3.0;
     *   gl_FragColor = vec4(color, 1.0);
     * }
     * `;
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   fillShader = createShader(vertSrc, fragSrc);
     *   noStroke();
     *   describe('A rotating torus with simulated directional lighting.');
     * }
     *
     * function draw() {
     *   background(20, 20, 40);
     *   let lightDir = [0.5, 0.5, 1.0];
     *   fillShader.setUniform('uLightDir', lightDir);
     *   shader(fillShader);
     *   rotateY(frameCount * 0.02);
     *   rotateX(frameCount * 0.02);
     *   torus(25, 10, 30, 30);
     * }
     * ```
     *
     * ```js example
     * let fillShader;
     *
     * let vertSrc = `
     * precision highp float;
     * attribute vec3 aPosition;
     * uniform mat4 uProjectionMatrix;
     * uniform mat4 uModelViewMatrix;
     * varying vec3 vPosition;
     * void main() {
     *   vPosition = aPosition;
     *   gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0);
     * }
     * `;
     *
     * let fragSrc = `
     * precision highp float;
     * uniform vec3 uLightPos;
     * uniform vec3 uFillColor;
     * varying vec3 vPosition;
     * void main() {
     *   float brightness = dot(normalize(uLightPos), normalize(vPosition));
     *   brightness = clamp(brightness, 0.0, 1.0);
     *   vec3 color = uFillColor * brightness;
     *   gl_FragColor = vec4(color, 1.0);
     * }
     * `;
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   fillShader = createShader(vertSrc, fragSrc);
     *   shader(fillShader);
     *   noStroke();
     *   describe('A square affected by both fill color and lighting, with lights controlled by mouse.');
     * }
     *
     * function draw() {
     *   let lightPos = [(mouseX - width / 2) / width,
     *     (mouseY - height / 2) / height, 1.0];
     *   fillShader.setUniform('uLightPos', lightPos);
     *   let fillColor = [map(mouseX, 0, width, 0, 1),
     *     map(mouseY, 0, height, 0, 1), 0.5];
     *   fillShader.setUniform('uFillColor', fillColor);
     *   plane(width, height);
     * }
     * ```
     *
     * <div>
     * <p>
     *
     * If you want to apply shaders to strokes or images, use the following methods:
     * - <a href="#/p5/strokeShader">strokeShader()</a> : Applies a shader to the stroke (outline) of shapes, allowing independent control over the stroke rendering using shaders.
     * - <a href="#/p5/imageShader">imageShader()</a> : Applies a shader to images or textures, controlling how the shader modifies their appearance during rendering.
     *
     * </p>
     * </div>
     *
     *
     * @method shader
     * @chainable
     * @param {p5.Shader} s <a href="#/p5.Shader">p5.Shader</a> object
     *                      to apply.
     *
     */
    fn.shader = function (s) {
      this._assert3d('shader');
      // p5._validateParameters('shader', arguments);

      this._renderer.shader(s);

      return this;
    };

    /**
     * Sets the <a href="#/p5.Shader">p5.Shader</a> object to apply for strokes.
     *
     * This method applies the given shader to strokes, allowing customization of
     * how lines and outlines are drawn in 3D space. The shader will be used for
     * strokes until <a href="#/p5/resetShader">resetShader()</a> is called or another
     * strokeShader is applied.
     *
     * The shader will be used for:
     * - Strokes only, regardless of whether the uniform `uStrokeWeight` is present.
     *
     * To further customize its behavior, refer to the various hooks provided by
     * the <a href="#/p5/baseStrokeShader">baseStrokeShader()</a> method, which allow
     * control over stroke weight, vertex positions, colors, and more.
     *
     * @method strokeShader
     * @chainable
     * @param {p5.Shader} s <a href="#/p5.Shader">p5.Shader</a> object
     *                      to apply for strokes.
     *
     *
     * @example
     * let animatedStrokeShader;
     *
     * let vertSrc = `
     * precision mediump int;
     *
     * uniform mat4 uModelViewMatrix;
     * uniform mat4 uProjectionMatrix;
     * uniform float uStrokeWeight;
     *
     * uniform bool uUseLineColor;
     * uniform vec4 uMaterialColor;
     *
     * uniform vec4 uViewport;
     * uniform int uPerspective;
     * uniform int uStrokeJoin;
     *
     * attribute vec4 aPosition;
     * attribute vec3 aTangentIn;
     * attribute vec3 aTangentOut;
     * attribute float aSide;
     * attribute vec4 aVertexColor;
     *
     * void main() {
     *   vec4 posp = uModelViewMatrix * aPosition;
     *   vec4 posqIn = uModelViewMatrix * (aPosition + vec4(aTangentIn, 0));
     *   vec4 posqOut = uModelViewMatrix * (aPosition + vec4(aTangentOut, 0));
     *
     *   float facingCamera = pow(
     *     abs(normalize(posqIn-posp).z),
     *     0.25
     *   );
     *
     *   float scale = mix(1., 0.995, facingCamera);
     *
     *   posp.xyz = posp.xyz * scale;
     *   posqIn.xyz = posqIn.xyz * scale;
     *   posqOut.xyz = posqOut.xyz * scale;
     *
     *   vec4 p = uProjectionMatrix * posp;
     *   vec4 qIn = uProjectionMatrix * posqIn;
     *   vec4 qOut = uProjectionMatrix * posqOut;
     *
     *   vec2 tangentIn = normalize((qIn.xy*p.w - p.xy*qIn.w) * uViewport.zw);
     *   vec2 tangentOut = normalize((qOut.xy*p.w - p.xy*qOut.w) * uViewport.zw);
     *
     *   vec2 curPerspScale;
     *   if(uPerspective == 1) {
     *     curPerspScale = (uProjectionMatrix * vec4(1, sign(uProjectionMatrix[1][1]), 0, 0)).xy;
     *   } else {
     *     curPerspScale = p.w / (0.5 * uViewport.zw);
     *   }
     *
     *   vec2 offset;
     *   vec2 tangent = aTangentIn == vec3(0.) ? tangentOut : tangentIn;
     *   vec2 normal = vec2(-tangent.y, tangent.x);
     *   float normalOffset = sign(aSide);
     *   float tangentOffset = abs(aSide) - 1.;
     *   offset = (normal * normalOffset + tangent * tangentOffset) *
     *     uStrokeWeight * 0.5;
     *
     *   gl_Position.xy = p.xy + offset.xy * curPerspScale;
     *   gl_Position.zw = p.zw;
     * }
     * `;
     *
     * let fragSrc = `
     * precision mediump float;
     * uniform float uTime;
     *
     * void main() {
     *   float wave = sin(gl_FragCoord.x * 0.1 + uTime) * 0.5 + 0.5;
     *   gl_FragColor = vec4(wave, 0.5, 1.0, 1.0);  // Animated color based on time
     * }
     * `;
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   animatedStrokeShader = createShader(vertSrc, fragSrc);
     *   strokeShader(animatedStrokeShader);
     *   strokeWeight(4);
     *
     *   describe('A hollow cube rotating continuously with its stroke colors changing dynamically over time against a static gray background.');
     * }
     *
     * function draw() {
     *   animatedStrokeShader.setUniform('uTime', millis() / 1000.0);
     *   background(250);
     *   rotateY(frameCount * 0.02);
     *   noFill();
     *   orbitControl();
     *   box(50);
     * }
     *
     * @example
     * let myShader;
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = baseStrokeShader().modify({
     *     'float random': `(vec2 p) {
     *       vec3 p3  = fract(vec3(p.xyx) * .1471);
     *       p3 += dot(p3, p3.yzx + 32.33);
     *       return fract((p3.x + p3.y) * p3.z);
     *     }`,
     *     'Inputs getPixelInputs': `(Inputs inputs) {
     *       // Modify alpha with dithering effect
     *       float a = inputs.color.a;
     *       inputs.color.a = 1.0;
     *       inputs.color *= random(inputs.position.xy) > a ? 0.0 : 1.0;
     *       return inputs;
     *     }`
     *   });
     * }
     *
     * function draw() {
     *   background(255);
     *   strokeShader(myShader);
     *   strokeWeight(12);
     *   beginShape();
     *   for (let i = 0; i <= 50; i++) {
     *     stroke(
     *       map(i, 0, 50, 150, 255),
     *       100 + 155 * sin(i / 5),
     *       255 * map(i, 0, 50, 1, 0)
     *     );
     *     vertex(
     *       map(i, 0, 50, 1, -1) * width / 3,
     *       50 * cos(i / 10 + frameCount / 80)
     *     );
     *   }
     *   endShape();
     * }
     */
    fn.strokeShader = function (s) {
      this._assert3d("strokeShader");
      // p5._validateParameters('strokeShader', arguments);

      this._renderer.strokeShader(s);

      return this;
    };

    /**
     * Sets the <a href="#/p5.Shader">p5.Shader</a> object to apply for images.
     *
     * This method allows the user to apply a custom shader to images, enabling
     * advanced visual effects such as pixel manipulation, color adjustments,
     * or dynamic behavior. The shader will be applied to the image drawn using
     * the <a href="#/p5/image">image()</a> function.
     *
     * The shader will be used exclusively for:
     * - `image()` calls, applying only when drawing 2D images.
     * - This shader will NOT apply to images used in <a href="#/p5/texture">texture()</a> or other 3D contexts.
     *   Any attempts to use the imageShader in these cases will be ignored.
     *
     * @method imageShader
     * @chainable
     * @param {p5.Shader} s <a href="#/p5.Shader">p5.Shader</a> object
     *                      to apply for images.
     *
     * @example
     * let img;
     * let imgShader;
     *
     * async function setup() {
     *   img = await loadImage('assets/outdoor_image.jpg');
     *
     *   createCanvas(200, 200, WEBGL);
     *   noStroke();
     *
     *   imgShader = createShader(`
     *     precision mediump float;
     *     attribute vec3 aPosition;
     *     attribute vec2 aTexCoord;
     *     varying vec2 vTexCoord;
     *     uniform mat4 uModelViewMatrix;
     *     uniform mat4 uProjectionMatrix;
     *
     *     void main() {
     *       vTexCoord = aTexCoord;
     *       gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0);
     *     }
     *   `, `
     *     precision mediump float;
     *     varying vec2 vTexCoord;
     *     uniform sampler2D uTexture;
     *     uniform vec2 uMousePos;
     *
     *     void main() {
     *       vec4 texColor = texture2D(uTexture, vTexCoord);
     *       // Adjust the color based on mouse position
     *       float r = uMousePos.x * texColor.r;
     *       float g = uMousePos.y * texColor.g;
     *       gl_FragColor = vec4(r, g, texColor.b, texColor.a);
     *     }
     *   `);
     *
     *   describe(
     *     'An image on a gray background where the colors change based on the mouse position.'
     *   );
     * }
     *
     * function draw() {
     *   background(220);
     *
     *   imageShader(imgShader);
     *
     *   // Map the mouse position to a range between 0 and 1
     *   let mousePosX = map(mouseX, 0, width, 0, 1);
     *   let mousePosY = map(mouseY, 0, height, 0, 1);
     *
     *   // Pass the mouse position to the shader as a uniform
     *   imgShader.setUniform('uMousePos', [mousePosX, mousePosY]);
     *
     *   // Bind the image texture to the shader
     *   imgShader.setUniform('uTexture', img);
     *
     *   image(img, -width / 2, -height / 2, width, height);
     * }
     *
     *
     * @example
     * let img;
     * let imgShader;
     *
     * async function setup() {
     *   img = await loadImage('assets/outdoor_image.jpg');
     *
     *   createCanvas(200, 200, WEBGL);
     *   noStroke();
     *
     *   imgShader = createShader(`
     *     precision mediump float;
     *     attribute vec3 aPosition;
     *     attribute vec2 aTexCoord;
     *     varying vec2 vTexCoord;
     *     uniform mat4 uModelViewMatrix;
     *     uniform mat4 uProjectionMatrix;
     *
     *     void main() {
     *       vTexCoord = aTexCoord;
     *       gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0);
     *     }
     *   `, `
     *     precision mediump float;
     *     varying vec2 vTexCoord;
     *     uniform sampler2D uTexture;
     *     uniform vec2 uMousePos;
     *
     *     void main() {
     *       // Distance from the current pixel to the mouse
     *       float distFromMouse = distance(vTexCoord, uMousePos);
     *
     *       // Adjust pixelation based on distance (closer = more detail, farther = blockier)
     *       float pixelSize = mix(0.002, 0.05, distFromMouse);
     *       vec2 pixelatedCoord = vec2(floor(vTexCoord.x / pixelSize) * pixelSize,
     *                                  floor(vTexCoord.y / pixelSize) * pixelSize);
     *
     *       vec4 texColor = texture2D(uTexture, pixelatedCoord);
     *       gl_FragColor = texColor;
     *     }
     *   `);
     *
     *   describe('A static image with a grid-like, pixelated effect created by the shader. Each cell in the grid alternates visibility, producing a dithered visual effect.');
     * }
     *
     * function draw() {
     *   background(220);
     *   imageShader(imgShader);
     *
     *   let mousePosX = map(mouseX, 0, width, 0, 1);
     *   let mousePosY = map(mouseY, 0, height, 0, 1);
     *
     *   imgShader.setUniform('uMousePos', [mousePosX, mousePosY]);
     *   imgShader.setUniform('uTexture', img);
     *   image(img, -width / 2, -height / 2, width, height);
     * }
     */
    fn.imageShader = function (s) {
      this._assert3d("imageShader");
      // p5._validateParameters('imageShader', arguments);

      this._renderer.imageShader(s);

      return this;
    };

    /**
     * Create a new shader that can change how fills are drawn. Pass the resulting
     * shader into the <a href="#/p5/shader">`shader()`</a> function to apply it
     * to any fills you draw.
     *
     * The main way to use `buildMaterialShader` is to pass a function in as a parameter.
     * This will let you create a shader using p5.strands.
     *
     * In your function, you can call *hooks* to change part of the shader. In a material
     * shader, these are the hooks available:
     * - <a href="#/p5/objectInputs">`objectInputs`</a>: Update vertices before any positioning has been applied. Your function gets run on every vertex.
     * - <a href="#/p5/worldInputs">`worldInputs`</a>: Update vertices after transformations have been applied. Your function gets run on every vertex.
     * - <a href="#/p5/cameraInputs">`cameraInputs`</a>: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex.
     * - <a href="#/p5/pixelInputs">`pixelInputs`</a>: Update property values on pixels on the surface of a shape. Your function gets run on every pixel.
     * - <a href="#/p5/combineColors">`combineColors`</a>: Control how the ambient, diffuse, and specular components of lighting are combined into a single color on the surface of a shape. Your function gets run on every pixel.
     * - <a href="#/p5/finalColor">`finalColor`</a>: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel.
     *
     * Read the linked reference page for each hook for more information about how to use them.
     *
     * One thing you can do with a material shader is animate the positions of vertices
     * over time:
     *
     * ```js example
     * let myShader;
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = buildMaterialShader(material);
     * }
     *
     * function material() {
     *   let time = uniformFloat();
     *   worldInputs.begin();
     *   worldInputs.position.y +=
     *     20 * sin(time * 0.001 + worldInputs.position.x * 0.05);
     *   worldInputs.end();
     * }
     *
     * function draw() {
     *   background(255);
     *   shader(myShader);
     *   myShader.setUniform('time', millis());
     *   lights();
     *   noStroke();
     *   fill('red');
     *   sphere(50);
     * }
     * ```
     *
     * There are also many uses in updating values per pixel. This can be a good
     * way to give your sketch texture and detail. For example, instead of having a single
     * shininess or metalness value for a whole shape, you could vary it in different spots on its surface:
     *
     * ```js example
     * let myShader;
     * let environment;
     *
     * async function setup() {
     *   environment = await loadImage('assets/outdoor_spheremap.jpg');
     *
     *   createCanvas(200, 200, WEBGL);
     *   myShader = buildMaterialShader(material);
     * }
     *
     * function material() {
     *   pixelInputs.begin();
     *   let factor = sin(
     *     TWO_PI * (pixelInputs.texCoord.x + pixelInputs.texCoord.y)
     *   );
     *   pixelInputs.shininess = mix(1, 100, factor);
     *   pixelInputs.metalness = factor;
     *   pixelInputs.end();
     * }
     *
     * function draw() {
     *   panorama(environment);
     *   ambientLight(100);
     *   imageLight(environment);
     *   rotateY(millis() * 0.001);
     *   shader(myShader);
     *   noStroke();
     *   fill(255);
     *   specularMaterial(150);
     *   sphere(50);
     * }
     * ```
     *
     * A technique seen often in games called *bump mapping* is to vary the
     * *normal*, which is the orientation of the surface, per pixel to create texture
     * rather than using many tightly packed vertices. Sometimes this can come from
     * bump images, but it can also be done generatively with math.
     *
     * ```js example
     * let myShader;
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = buildMaterialShader(material);
     * }
     *
     * function material() {
     *   pixelInputs.begin();
     *   pixelInputs.normal.x += 0.2 * sin(
     *     sin(TWO_PI * dot(pixelInputs.texCoord.yx, vec2(10, 25)))
     *   );
     *   pixelInputs.normal.y += 0.2 * sin(
     *     sin(TWO_PI * dot(pixelInputs.texCoord, vec2(10, 25)))
     *   );
     *   pixelInputs.normal = normalize(pixelInputs.normal);
     *   pixelInputs.end();
     * }
     *
     * function draw() {
     *   background(255);
     *   shader(myShader);
     *   ambientLight(150);
     *   pointLight(
     *     255, 255, 255,
     *     100*cos(frameCount*0.04), -50, 100*sin(frameCount*0.04)
     *   );
     *   noStroke();
     *   fill('red');
     *   shininess(200);
     *   specularMaterial(255);
     *   sphere(50);
     * }
     * ```
     *
     * You can also update the final color directly instead of modifying
     * lighting settings. Sometimes in photographs, a light source is placed
     * behind the subject to create *rim lighting,* where the edges of the
     * subject are lit up. This can be simulated by adding white to the final
     * color on parts of the shape that are facing away from the camera.
     *
     * ```js example
     * let myShader;
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = buildMaterialShader(material);
     * }
     *
     * function material() {
     *   let myNormal = sharedVec3();
     *
     *   pixelInputs.begin();
     *   myNormal = pixelInputs.normal;
     *   pixelInputs.end();
     *
     *   finalColor.begin();
     *   finalColor.set(mix(
     *     [1, 1, 1, 1],
     *     finalColor.color,
     *     abs(dot(myNormal, [0, 0, 1]))
     *   ));
     *   finalColor.end();
     * }
     *
     * function draw() {
     *   background(255);
     *   rotateY(millis() * 0.001);
     *   shader(myShader);
     *   lights();
     *   noStroke();
     *   fill('red');
     *   torus(30);
     * }
     * ```
     *
     * Like the `modify()` method on shaders,
     * advanced users can also fill in hooks using <a href="https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_on_the_web/GLSL_Shaders" target="_blank">GLSL</a>
     * instead of JavaScript.
     * Read the <a href="#/p5.Shader/modify">reference entry for `modify()`</a>
     * for more info.
     *
     * @method buildMaterialShader
     * @submodule p5.strands
     * @beta
     * @param {Function} callback A function building a p5.strands shader.
     * @returns {p5.Shader} The material shader.
     */
    /**
     * @method buildMaterialShader
     * @param {Object} hooks An object specifying p5.strands hooks in GLSL.
     * @returns {p5.Shader} The material shader.
     */
    fn.buildMaterialShader = function (cb) {
      return this.baseMaterialShader().modify(cb);
    };

    /**
     * Loads a new shader from a file that can change how fills are drawn. Pass the resulting
     * shader into the <a href="#/p5/shader">`shader()`</a> function to apply it
     * to any fills you draw.
     *
     * Since this function loads data from another file, it returns a `Promise`.
     * Use it in an `async function setup`, and `await` its result.
     *
     * ```js
     * let myShader;
     * async function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = await loadMaterialShader('myMaterial.js');
     * }
     *
     * function draw() {
     *   background(255);
     *   shader(myShader);
     *   myShader.setUniform('time', millis());
     *   lights();
     *   noStroke();
     *   fill('red');
     *   sphere(50);
     * }
     * ```
     *
     * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For
     * example, you might use the `worldInputs` hook to change each vertex, or you
     * might use the `pixelInputs` hook to change each pixel on the surface of a shape.
     *
     * ```js
     * // myMaterial.js
     * let time = uniformFloat();
     * worldInputs.begin();
     * worldInputs.position.y +=
     *   20 * sin(time * 0.001 + worldInputs.position.x * 0.05);
     * worldInputs.end();
     * ```
     *
     * Read the reference for <a href="#/p5/buildMaterialShader">`buildMaterialShader`</a>,
     * the version of `loadMaterialShader` that takes in a function instead of a separate file,
     * for a full list of hooks you can use and examples for each.
     *
     * The second parameter, `successCallback`, is optional. If a function is passed, as in
     * `loadMaterialShader('myShader.js', onLoaded)`, then the `onLoaded()` function will be called
     * once the shader loads. The shader will be passed to `onLoaded()` as its only argument.
     * The return value of `handleData()`, if present, will be used as the final return value of
     * `loadMaterialShader('myShader.js', onLoaded)`.
     *
     * @method loadMaterialShader
     * @submodule p5.strands
     * @beta
     * @param {String} url The URL of your p5.strands JavaScript file.
     * @param {Function} [onSuccess] A callback function to run when loading completes.
     * @param {Function} [onFailure] A callback function to run when loading fails.
     * @returns {Promise<p5.Shader>} The material shader.
     */
    fn.loadMaterialShader = async function (url, onSuccess, onFail) {
      try {
        const cb = await urlToStrandsCallback(url);
        let shader = withGlobalStrands(this, () => this.buildMaterialShader(cb));
        if (onSuccess) {
          shader = onSuccess(shader) || shader;
        }
        return shader;
      } catch (e) {
        console.error(e);
        if (onFail) {
          onFail(e);
        }
      }
    };

    /**
     * Returns the default shader used for fills when lights or textures are used.
     *
     * Calling <a href="#/p5/buildMaterialShader">`buildMaterialShader(shaderFunction)`</a>
     * is equivalent to calling `baseMaterialShader().modify(shaderFunction)`.
     *
     * Read <a href="#/p5/buildMaterialShader">the `buildMaterialShader` reference</a> or
     * call `baseMaterialShader().inspectHooks()` for more information on what you can do with
     * the base material shader.
     *
     * @method baseMaterialShader
     * @submodule p5.strands
     * @beta
     * @returns {p5.Shader} The base material shader.
     */
    fn.baseMaterialShader = function () {
      this._assert3d("baseMaterialShader");
      return this._renderer.baseMaterialShader();
    };

    /**
     * Returns the base shader used for filters.
     *
     * Calling <a href="#/p5/buildMaterialShader">`buildFilterShader(shaderFunction)`</a>
     * is equivalent to calling `baseFilterShader().modify(shaderFunction)`.
     *
     * Read <a href="#/p5/buildFilterShader">the `buildFilterShader` reference</a> or
     * call `baseFilterShader().inspectHooks()` for more information on what you can do with
     * the base filter shader.
     *
     * @method baseFilterShader
     * @submodule p5.strands
     * @beta
     * @returns {p5.Shader} The base filter shader.
     */
    fn.baseFilterShader = function () {
      return (this._renderer.filterRenderer || this._renderer).baseFilterShader();
    };

    /**
     * Create a new shader that can change how fills are drawn, based on the material used
     * when <a href="#/p5/normalMaterial">`normalMaterial()`</a> is active. Pass the resulting
     * shader into the <a href="#/p5/shader">`shader()`</a> function to apply it to any fills
     * you draw.
     *
     * The main way to use `buildNormalShader` is to pass a function in as a parameter.
     * This will let you create a shader using p5.strands.
     *
     * In your function, you can call *hooks* to change part of the shader. In a material
     * shader, these are the hooks available:
     * - <a href="#/p5/objectInputs">`objectInputs`</a>: Update vertices before any positioning has been applied. Your function gets run on every vertex.
     * - <a href="#/p5/worldInputs">`worldInputs`</a>: Update vertices after transformations have been applied. Your function gets run on every vertex.
     * - <a href="#/p5/cameraInputs">`cameraInputs`</a>: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex.
     * - <a href="#/p5/finalColor">`finalColor`</a>: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel.
     *
     * Read the linked reference page for each hook for more information about how to use them.
     *
     * One thing you may want to do is update the position of all the vertices in an object over time:
     *
     * ```js example
     * let myShader;
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = buildNormalShader(material);
     * }
     *
     * function material() {
     *   let time = uniformFloat();
     *   worldInputs.begin();
     *   worldInputs.position.y +=
     *     20. * sin(time * 0.001 + worldInputs.position.x * 0.05);
     *   worldInputs.end();
     * }
     *
     * function draw() {
     *   background(255);
     *   shader(myShader);
     *   myShader.setUniform('time', millis());
     *   noStroke();
     *   sphere(50);
     * }
     * ```
     *
     * You may also want to change the colors used. By default, the x, y, and z values of the orientation
     * of the surface are mapped directly to red, green, and blue. But you can pick different colors:
     *
     * ```js example
     * let myShader;
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = buildNormalShader(material);
     * }
     *
     * function material() {
     *   cameraInputs.begin();
     *   cameraInputs.normal = abs(cameraInputs.normal);
     *   cameraInputs.end();
     *
     *   finalColor.begin();
     *   // Map the r, g, and b values of the old normal to new colors
     *   // instead of just red, green, and blue:
     *   let newColor =
     *     finalColor.color.r * [89, 240, 232] / 255 +
     *     finalColor.color.g * [240, 237, 89] / 255 +
     *     finalColor.color.b * [205, 55, 222] / 255;
     *   newColor = newColor / (finalColor.color.r + finalColor.color.g + finalColor.color.b);
     *   finalColor.set([newColor.r, newColor.g, newColor.b, finalColor.color.a]);
     *   finalColor.end();
     * }
     *
     * function draw() {
     *   background(255);
     *   shader(myShader);
     *   noStroke();
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.015);
     *   box(100);
     * }
     * ```
     *
     * Like the `modify()` method on shaders,
     * advanced users can also fill in hooks using <a href="https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_on_the_web/GLSL_Shaders" target="_blank">GLSL</a>
     * instead of JavaScript.
     * Read the <a href="#/p5.Shader/modify">reference entry for `modify()`</a>
     * for more info.
     *
     * @method buildNormalShader
     * @submodule p5.strands
     * @beta
     * @param {Function} callback A function building a p5.strands shader.
     * @returns {p5.Shader} The normal shader.
     */
    /**
     * @method buildNormalShader
     * @param {Object} hooks An object specifying p5.strands hooks in GLSL.
     * @returns {p5.Shader} The normal shader.
     */
    fn.buildNormalShader = function (cb) {
      return this.baseNormalShader().modify(cb);
    };

    /**
     * Loads a new shader from a file that can change how fills are drawn, based on the material used
     * when <a href="#/p5/normalMaterial">`normalMaterial()`</a> is active. Pass the resulting
     * shader into the <a href="#/p5/shader">`shader()`</a> function to apply it
     * to any fills you draw.
     *
     * Since this function loads data from another file, it returns a `Promise`.
     * Use it in an `async function setup`, and `await` its result.
     *
     * ```js
     * let myShader;
     * async function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = await loadNormalShader('myMaterial.js');
     * }
     *
     * function draw() {
     *   background(255);
     *   shader(myShader);
     *   myShader.setUniform('time', millis());
     *   lights();
     *   noStroke();
     *   fill('red');
     *   sphere(50);
     * }
     * ```
     *
     * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For
     * example, you might use the `worldInputs` hook to change each vertex, or you
     * might use the `finalColor` hook to change the color of each pixel on the surface of a shape.
     *
     * ```js
     * // myMaterial.js
     * let time = uniformFloat();
     * worldInputs.begin();
     * worldInputs.position.y +=
     *   20 * sin(time * 0.001 + worldInputs.position.x * 0.05);
     * worldInputs.end();
     * ```
     *
     * Read the reference for <a href="#/p5/buildNormalShader">`buildNormalShader`</a>,
     * the version of `loadNormalShader` that takes in a function instead of a separate file,
     * for a full list of hooks you can use and examples for each.
     *
     * The second parameter, `successCallback`, is optional. If a function is passed, as in
     * `loadNormalShader('myShader.js', onLoaded)`, then the `onLoaded()` function will be called
     * once the shader loads. The shader will be passed to `onLoaded()` as its only argument.
     * The return value of `handleData()`, if present, will be used as the final return value of
     * `loadNormalShader('myShader.js', onLoaded)`.
     *
     * @method loadNormalShader
     * @submodule p5.strands
     * @beta
     * @param {String} url The URL of your p5.strands JavaScript file.
     * @param {Function} [onSuccess] A callback function to run when loading completes.
     * @param {Function} [onFailure] A callback function to run when loading fails.
     * @returns {Promise<p5.Shader>} The normal shader.
     */
    fn.loadNormalShader = async function (url, onSuccess, onFail) {
      try {
        const cb = await urlToStrandsCallback(url);
        let shader = this.withGlobalStrands(this, () =>
          this.buildNormalShader(cb),
        );
        if (onSuccess) {
          shader = onSuccess(shader) || shader;
        }
        return shader;
      } catch (e) {
        console.error(e);
        if (onFail) {
          onFail(e);
        }
      }
    };

    /**
     * Returns the default shader used for fills when
     * <a href="#/p5/normalMaterial">`normalMaterial()`</a> is activated.
     *
     * Calling <a href="#/p5/buildNormalShader">`buildNormalShader(shaderFunction)`</a>
     * is equivalent to calling `baseNormalShader().modify(shaderFunction)`.
     *
     * Read <a href="#/p5/buildNormalShader">the `buildNormalShader` reference</a> or
     * call `baseNormalShader().inspectHooks()` for more information on what you can do with
     * the base normal shader.
     *
     * @method baseNormalShader
     * @submodule p5.strands
     * @beta
     * @returns {p5.Shader} The base material shader.
     */
    fn.baseNormalShader = function () {
      this._assert3d("baseNormalShader");
      return this._renderer.baseNormalShader();
    };

    /**
     * Create a new shader that can change how fills are drawn, based on the default shader
     * used when no lights or textures are applied. Pass the resulting
     * shader into the <a href="#/p5/shader">`shader()`</a> function to apply it
     * to any fills you draw.
     *
     * The main way to use `buildColorShader` is to pass a function in as a parameter.
     * This will let you create a shader using p5.strands.
     *
     * In your function, you can call *hooks* to change part of the shader. In a material
     * shader, these are the hooks available:
     * - <a href="#/p5/objectInputs">`objectInputs`</a>: Update vertices before any positioning has been applied. Your function gets run on every vertex.
     * - <a href="#/p5/worldInputs">`worldInputs`</a>: Update vertices after transformations have been applied. Your function gets run on every vertex.
     * - <a href="#/p5/cameraInputs">`cameraInputs`</a>: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex.
     * - <a href="#/p5/finalColor">`finalColor`</a>: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel.
     *
     * Read the linked reference page for each hook for more information about how to use them.
     *
     * One thing you might want to do is modify the position of every vertex over time:
     *
     * ```js example
     * let myShader;
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = buildColorShader(material);
     * }
     *
     * function material() {
     *   let time = uniformFloat();
     *   worldInputs.begin();
     *   worldInputs.position.y +=
     *     20 * sin(time * 0.001 + worldInputs.position.x * 0.05);
     *   worldInputs.end();
     * }
     *
     * function draw() {
     *   background(255);
     *   shader(myShader);
     *   myShader.setUniform('time', millis());
     *   noStroke();
     *   fill('red');
     *   circle(0, 0, 50);
     * }
     * ```
     *
     * Like the `modify()` method on shaders,
     * advanced users can also fill in hooks using <a href="https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_on_the_web/GLSL_Shaders" target="_blank">GLSL</a>
     * instead of JavaScript.
     * Read the <a href="#/p5.Shader/modify">reference entry for `modify()`</a>
     * for more info.
     *
     * @method buildColorShader
     * @submodule p5.strands
     * @beta
     * @param {Function} callback A function building a p5.strands shader.
     * @returns {p5.Shader} The color shader.
     */
    /**
     * @method buildColorShader
     * @param {Object} hooks An object specifying p5.strands hooks in GLSL.
     * @returns {p5.Shader} The color shader.
     */
    fn.buildColorShader = function (cb) {
      return this.baseColorShader().modify(cb);
    };

    /**
     * Loads a new shader from a file that can change how fills are drawn, based on the material used
     * when no lights or textures are active. Pass the resulting
     * shader into the <a href="#/p5/shader">`shader()`</a> function to apply it
     * to any fills you draw.
     *
     * Since this function loads data from another file, it returns a `Promise`.
     * Use it in an `async function setup`, and `await` its result.
     *
     * ```js
     * let myShader;
     * async function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = await loadColorShader('myMaterial.js');
     * }
     *
     * function draw() {
     *   background(255);
     *   shader(myShader);
     *   myShader.setUniform('time', millis());
     *   lights();
     *   noStroke();
     *   fill('red');
     *   circle(0, 0, 50);
     * }
     * ```
     *
     * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For
     * example, you might use the `worldInputs` hook to change each vertex, or you
     * might use the `finalColor` hook to change the color of each pixel on the surface of a shape.
     *
     * ```js
     * // myMaterial.js
     * let time = uniformFloat();
     * worldInputs.begin();
     * worldInputs.position.y +=
     *   20 * sin(time * 0.001 + worldInputs.position.x * 0.05);
     * worldInputs.end();
     * ```
     *
     * Read the reference for <a href="#/p5/buildColorShader">`buildColorShader`</a>,
     * the version of `loadColorShader` that takes in a function instead of a separate file,
     * for a full list of hooks you can use and examples for each.
     *
     * The second parameter, `successCallback`, is optional. If a function is passed, as in
     * `loadColorShader('myShader.js', onLoaded)`, then the `onLoaded()` function will be called
     * once the shader loads. The shader will be passed to `onLoaded()` as its only argument.
     * The return value of `handleData()`, if present, will be used as the final return value of
     * `loadColorShader('myShader.js', onLoaded)`.
     *
     * @method loadColorShader
     * @submodule p5.strands
     * @beta
     * @param {String} url The URL of your p5.strands JavaScript file.
     * @param {Function} [onSuccess] A callback function to run when loading completes.
     * @param {Function} [onFailure] A callback function to run when loading fails.
     * @returns {Promise<p5.Shader>} The color shader.
     */
    fn.loadColorShader = async function (url, onSuccess, onFail) {
      try {
        const cb = await urlToStrandsCallback(url);
        let shader = withGlobalStrands(this, () => this.buildColorShader(cb));
        if (onSuccess) {
          shader = onSuccess(shader) || shader;
        }
        return shader;
      } catch (e) {
        console.error(e);
        if (onFail) {
          onFail(e);
        }
      }
    };

    /**
     * Returns the default shader used for fills when no lights or textures are activate.
     *
     * Calling <a href="#/p5/buildColorShader">`buildColorShader(shaderFunction)`</a>
     * is equivalent to calling `baseColorShader().modify(shaderFunction)`.
     *
     * Read <a href="#/p5/buildColorShader">the `buildColorShader` reference</a> or
     * call `baseColorShader().inspectHooks()` for more information on what you can do with
     * the base color shader.
     *
     * @method baseColorShader
     * @submodule p5.strands
     * @beta
     * @returns {p5.Shader} The base color shader.
     */
    fn.baseColorShader = function () {
      this._assert3d("baseColorShader");
      return this._renderer.baseColorShader();
    };

    /**
     * Create a new shader that can change how strokes are drawn, based on the default
     * shader used for strokes. Pass the resulting shader into the
     * <a href="#/p5/strokeShader">`strokeShader()`</a> function to apply it to any
     * strokes you draw.
     *
     * The main way to use `buildStrokeShader` is to pass a function in as a parameter.
     * This will let you create a shader using p5.strands.
     *
     * In your function, you can call *hooks* to change part of the shader. In a material
     * shader, these are the hooks available:
     * - <a href="#/p5/objectInputs">`objectInputs`</a>: Update vertices before any positioning has been applied. Your function gets run on every vertex.
     * - <a href="#/p5/worldInputs">`worldInputs`</a>: Update vertices after transformations have been applied. Your function gets run on every vertex.
     * - <a href="#/p5/cameraInputs">`cameraInputs`</a>: Update vertices after transformations have been applied, relative to the camera. Your function gets run on every vertex.
     * - <a href="#/p5/pixelInputs">`pixelInputs`</a>: Update property values on pixels on the surface of a shape. Your function gets run on every pixel.
     * - <a href="#/p5/finalColor">`finalColor`</a>: Update or replace the pixel color on the surface of a shape. Your function gets run on every pixel.
     *
     * Read the linked reference page for each hook for more information about how to use them.
     *
     * One thing you might want to do is update the color of a stroke per pixel. Here, it is being used
     * to create a soft texture:
     *
     * ```js example
     * let myShader;
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = buildStrokeShader(material);
     * }
     *
     * function material() {
     *   pixelInputs.begin();
     *   let opacity = 1 - smoothstep(
     *     0,
     *     15,
     *     length(pixelInputs.position - pixelInputs.center)
     *   );
     *   pixelInputs.color.a *= opacity;
     *   pixelInputs.end();
     * }
     *
     * function draw() {
     *   background(255);
     *   strokeShader(myShader);
     *   strokeWeight(30);
     *   line(
     *     -width/3,
     *     sin(millis()*0.001) * height/4,
     *     width/3,
     *     sin(millis()*0.001 + 1) * height/4
     *   );
     * }
     * ```
     *
     * Rather than using opacity, we could use a form of *dithering* to get a different
     * texture. This involves using only fully opaque or transparent pixels. Here, we
     * randomly choose which pixels to be transparent:
     *
     * ```js example
     * let myShader;
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = buildStrokeShader(material);
     * }
     *
     * function material() {
     *   pixelInputs.begin();
     *   // Replace alpha in the color with dithering by
     *   // randomly setting pixel colors to 0 based on opacity
     *   let a = 1;
     *   if (noise(pixelInputs.position.xy) > pixelInputs.color.a) {
     *     a = 0;
     *   }
     *   pixelInputs.color.a = a;
     *   pixelInputs.end();
     * }
     *
     * function draw() {
     *   background(255);
     *   strokeShader(myShader);
     *   strokeWeight(10);
     *   beginShape();
     *   for (let i = 0; i <= 50; i++) {
     *     stroke(
     *       0,
     *       255
     *         * map(i, 0, 20, 0, 1, true)
     *         * map(i, 30, 50, 1, 0, true)
     *     );
     *     vertex(
     *       map(i, 0, 50, -1, 1) * width/3,
     *       50 * sin(i/10 + frameCount/100)
     *     );
     *   }
     *   endShape();
     * }
     * ```
     *
     * You might also want to update some properties per vertex, such as the stroke
     * thickness. This lets you create a more varied line:
     *
     * ```js example
     * let myShader;
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = buildStrokeShader(material);
     * }
     *
     * function material() {
     *   let time = uniformFloat();
     *   worldInputs.begin();
     *   // Add a somewhat random offset to the weight
     *   // that varies based on position and time
     *   let scale = 0.5 + noise(
     *     worldInputs.position.x * 0.01,
     *     worldInputs.position.y * 0.01,
     *     time * 0.0005
     *   );
     *   worldInputs.weight *= scale;
     *   worldInputs.end();
     * }
     *
     * function draw() {
     *   background(255);
     *   strokeShader(myShader);
     *   myShader.setUniform('time', millis());
     *   strokeWeight(10);
     *   beginShape();
     *   for (let i = 0; i <= 50; i++) {
     *     let r = map(i, 0, 50, 0, width/3);
     *     let x = r*cos(i*0.2);
     *     let y = r*sin(i*0.2);
     *     vertex(x, y);
     *   }
     *   endShape();
     * }
     * ```
     *
     * Like the `modify()` method on shaders,
     * advanced users can also fill in hooks using <a href="https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_on_the_web/GLSL_Shaders" target="_blank">GLSL</a>
     * instead of JavaScript.
     * Read the <a href="#/p5.Shader/modify">reference entry for `modify()`</a>
     * for more info.
     *
     * @method buildStrokeShader
     * @submodule p5.strands
     * @beta
     * @param {Function} callback A function building a p5.strands shader.
     * @returns {p5.Shader} The stroke shader.
     */
    /**
     * @method buildStrokeShader
     * @param {Object} hooks An object specifying p5.strands hooks in GLSL.
     * @returns {p5.Shader} The stroke shader.
     */
    fn.buildStrokeShader = function (cb) {
      return this.baseStrokeShader().modify(cb);
    };

    /**
     * Loads a new shader from a file that can change how strokes are drawn. Pass the resulting
     * shader into the <a href="#/p5/strokeShader">`strokeShader()`</a> function to apply it
     * to any strokes you draw.
     *
     * Since this function loads data from another file, it returns a `Promise`.
     * Use it in an `async function setup`, and `await` its result.
     *
     * ```js
     * let myShader;
     * async function setup() {
     *   createCanvas(200, 200, WEBGL);
     *   myShader = await loadStrokeShader('myMaterial.js');
     * }
     *
     * function draw() {
     *   background(255);
     *   strokeShader(myShader);
     *   strokeWeight(30);
     *   line(
     *     -width/3,
     *     sin(millis()*0.001) * height/4,
     *     width/3,
     *     sin(millis()*0.001 + 1) * height/4
     *   );
     * }
     * ```
     *
     * Inside your shader file, you can call p5.strands hooks to change parts of the shader. For
     * example, you might use the `worldInputs` hook to change each vertex, or you
     * might use the `pixelInputs` hook to change each pixel on the surface of a stroke.
     *
     * ```js
     * // myMaterial.js
     * pixelInputs.begin();
     * let opacity = 1 - smoothstep(
     *   0,
     *   15,
     *   length(pixelInputs.position - pixelInputs.center)
     * );
     * pixelInputs.color.a *= opacity;
     * pixelInputs.end();
     * ```
     *
     * Read the reference for <a href="#/p5/buildStrokeShader">`buildStrokeShader`</a>,
     * the version of `loadStrokeShader` that takes in a function instead of a separate file,
     * for a full list of hooks you can use and examples for each.
     *
     * The second parameter, `successCallback`, is optional. If a function is passed, as in
     * `loadStrokeShader('myShader.js', onLoaded)`, then the `onLoaded()` function will be called
     * once the shader loads. The shader will be passed to `onLoaded()` as its only argument.
     * The return value of `handleData()`, if present, will be used as the final return value of
     * `loadStrokeShader('myShader.js', onLoaded)`.
     *
     * @method loadStrokeShader
     * @submodule p5.strands
     * @beta
     * @param {String} url The URL of your p5.strands JavaScript file.
     * @param {Function} [onSuccess] A callback function to run when loading completes.
     * @param {Function} [onFailure] A callback function to run when loading fails.
     * @returns {Promise<p5.Shader>} The stroke shader.
     */
    fn.loadStrokeShader = async function (url, onSuccess, onFail) {
      try {
        const cb = await urlToStrandsCallback(url);
        let shader = withGlobalStrands(this, () => this.buildStrokeShader(cb));
        if (onSuccess) {
          shader = onSuccess(shader) || shader;
        }
        return shader;
      } catch (e) {
        console.error(e);
        if (onFail) {
          onFail(e);
        }
      }
    };

    /**
     * Returns the default shader used for strokes.
     *
     * Calling <a href="#/p5/buildStrokeShader">`buildStrokeShader(shaderFunction)`</a>
     * is equivalent to calling `baseStrokeShader().modify(shaderFunction)`.
     *
     * Read <a href="#/p5/buildStrokeShader">the `buildStrokeShader` reference</a> or
     * call `baseStrokeShader().inspectHooks()` for more information on what you can do with
     * the base material shader.
     *
     * @method baseStrokeShader
     * @submodule p5.strands
     * @beta
     * @returns {p5.Shader} The base material shader.
     */
    fn.baseStrokeShader = function () {
      this._assert3d("baseStrokeShader");
      return this._renderer.baseStrokeShader();
    };

    /**
     * Restores the default shaders.
     *
     * `resetShader()` deactivates any shaders previously applied by
     * <a href="#/p5/shader">shader()</a>, <a href="#/p5/strokeShader">strokeShader()</a>,
     * or <a href="#/p5/imageShader">imageShader()</a>.
     *
     * Note: Shaders can only be used in WebGL mode.
     *
     * @method resetShader
     * @chainable
     *
     * @example
     * // Create a string with the vertex shader program.
     * // The vertex shader is called for each vertex.
     * let vertSrc = `
     * attribute vec3 aPosition;
     * attribute vec2 aTexCoord;
     * uniform mat4 uProjectionMatrix;
     * uniform mat4 uModelViewMatrix;
     * varying vec2 vTexCoord;
     *
     * void main() {
     *   vTexCoord = aTexCoord;
     *   vec4 position = vec4(aPosition, 1.0);
     *   gl_Position = uProjectionMatrix * uModelViewMatrix * position;
     * }
     * `;
     *
     * // Create a string with the fragment shader program.
     * // The fragment shader is called for each pixel.
     * let fragSrc = `
     * precision mediump float;
     * varying vec2 vTexCoord;
     *
     * void main() {
     *   vec2 uv = vTexCoord;
     *   vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0));
     *   gl_FragColor = vec4(color, 1.0);
     * }
     * `;
     *
     * let myShader;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Shader object.
     *   myShader = createShader(vertSrc, fragSrc);
     *
     *   describe(
     *     'Two rotating cubes on a gray background. The left one has a blue-purple gradient on each face. The right one is red.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw a box using the p5.Shader.
     *   // shader() sets the active shader to myShader.
     *   shader(myShader);
     *   push();
     *   translate(-25, 0, 0);
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.01);
     *   box(width / 4);
     *   pop();
     *
     *   // Draw a box using the default fill shader.
     *   // resetShader() restores the default fill shader.
     *   resetShader();
     *   fill(255, 0, 0);
     *   push();
     *   translate(25, 0, 0);
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.01);
     *   box(width / 4);
     *   pop();
     * }
     */
    fn.resetShader = function () {
      this._renderer.resetShader();
      return this;
    };

    /**
     * Sets the texture that will be used on shapes.
     *
     * A texture is like a skin that wraps around a shape. `texture()` works with
     * built-in shapes, such as <a href="#/p5/square">square()</a> and
     * <a href="#/p5/sphere">sphere()</a>, and custom shapes created with
     * functions such as <a href="#/p5/buildGeometry">buildGeometry()</a>. To
     * texture a geometry created with <a href="#/p5/beginShape">beginShape()</a>,
     * uv coordinates must be passed to each
     * <a href="#/p5/vertex">vertex()</a> call.
     *
     * The parameter, `tex`, is the texture to apply. `texture()` can use a range
     * of sources including images, videos, and offscreen renderers such as
     * <a href="#/p5.Graphics">p5.Graphics</a> and
     * <a href="#/p5.Framebuffer">p5.Framebuffer</a> objects.
     *
     * To texture a geometry created with <a href="#/p5/beginShape">beginShape()</a>,
     * you will need to specify uv coordinates in <a href="#/p5/vertex">vertex()</a>.
     *
     * Note: `texture()` can only be used in WebGL mode.
     *
     * @method texture
     * @param {p5.Image|p5.MediaElement|p5.Graphics|p5.Texture|p5.Framebuffer|p5.FramebufferTexture} tex media to use as the texture.
     * @chainable
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load an image and create a p5.Image object.
     *   img = await loadImage('assets/laDefense.jpg');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A spinning cube with an image of a ceiling on each face.');
     * }
     *
     * function draw() {
     *   background(0);
     *
     *   // Rotate around the x-, y-, and z-axes.
     *   rotateZ(frameCount * 0.01);
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.01);
     *
     *   // Apply the image as a texture.
     *   texture(img);
     *
     *   // Draw the box.
     *   box(50);
     * }
     *
     * @example
     * let pg;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Graphics object.
     *   pg = createGraphics(100, 100);
     *
     *   // Draw a circle to the p5.Graphics object.
     *   pg.background(200);
     *   pg.circle(50, 50, 30);
     *
     *   describe('A spinning cube with circle at the center of each face.');
     * }
     *
     * function draw() {
     *   background(0);
     *
     *   // Rotate around the x-, y-, and z-axes.
     *   rotateZ(frameCount * 0.01);
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.01);
     *
     *   // Apply the p5.Graphics object as a texture.
     *   texture(pg);
     *
     *   // Draw the box.
     *   box(50);
     * }
     *
     * @example
     * let vid;
     *
     * function setup() {
     *   // Load a video and create a p5.MediaElement object.
     *   vid = createVideo('assets/fingers.mov');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Hide the video.
     *   vid.hide();
     *
     *   // Set the video to loop.
     *   vid.loop();
     *
     *   describe('A rectangle with video as texture');
     * }
     *
     * function draw() {
     *   background(0);
     *
     *   // Rotate around the y-axis.
     *   rotateY(frameCount * 0.01);
     *
     *   // Apply the video as a texture.
     *   texture(vid);
     *
     *   // Draw the rectangle.
     *   rect(-40, -40, 80, 80);
     * }
     *
     * @example
     * let vid;
     *
     * function setup() {
     *   // Load a video and create a p5.MediaElement object.
     *   vid = createVideo('assets/fingers.mov');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Hide the video.
     *   vid.hide();
     *
     *   // Set the video to loop.
     *   vid.loop();
     *
     *   describe('A rectangle with video as texture');
     * }
     *
     * function draw() {
     *   background(0);
     *
     *   // Rotate around the y-axis.
     *   rotateY(frameCount * 0.01);
     *
     *   // Set the texture mode.
     *   textureMode(NORMAL);
     *
     *   // Apply the video as a texture.
     *   texture(vid);
     *
     *   // Draw a custom shape using uv coordinates.
     *   beginShape();
     *   vertex(-40, -40, 0, 0);
     *   vertex(40, -40, 1, 0);
     *   vertex(40, 40, 1, 1);
     *   vertex(-40, 40, 0, 1);
     *   endShape();
     * }
     */
    fn.texture = function (tex) {
      this._assert3d("texture");
      // p5._validateParameters('texture', arguments);

      // NOTE: make generic or remove need for
      if (tex.gifProperties) {
        tex._animateGif(this);
      }

      this._renderer.texture(tex);

      return this;
    };

    /**
     * Changes the coordinate system used for textures when they’re applied to
     * custom shapes.
     *
     * In order for <a href="#/p5/texture">texture()</a> to work, a shape needs a
     * way to map the points on its surface to the pixels in an image. Built-in
     * shapes such as <a href="#/p5/rect">rect()</a> and
     * <a href="#/p5/box">box()</a> already have these texture mappings based on
     * their vertices. Custom shapes created with
     * <a href="#/p5/vertex">vertex()</a> require texture mappings to be passed as
     * uv coordinates.
     *
     * Each call to <a href="#/p5/vertex">vertex()</a> must include 5 arguments,
     * as in `vertex(x, y, z, u, v)`, to map the vertex at coordinates `(x, y, z)`
     * to the pixel at coordinates `(u, v)` within an image. For example, the
     * corners of a rectangular image are mapped to the corners of a rectangle by default:
     *
     * ```js
     * // Apply the image as a texture.
     * texture(img);
     *
     * // Draw the rectangle.
     * rect(0, 0, 30, 50);
     * ```
     *
     * If the image in the code snippet above has dimensions of 300 x 500 pixels,
     * the same result could be achieved as follows:
     *
     * ```js
     * // Apply the image as a texture.
     * texture(img);
     *
     * // Draw the rectangle.
     * beginShape();
     *
     * // Top-left.
     * // u: 0, v: 0
     * vertex(0, 0, 0, 0, 0);
     *
     * // Top-right.
     * // u: 300, v: 0
     * vertex(30, 0, 0, 300, 0);
     *
     * // Bottom-right.
     * // u: 300, v: 500
     * vertex(30, 50, 0, 300, 500);
     *
     * // Bottom-left.
     * // u: 0, v: 500
     * vertex(0, 50, 0, 0, 500);
     *
     * endShape();
     * ```
     *
     * `textureMode()` changes the coordinate system for uv coordinates.
     *
     * The parameter, `mode`, accepts two possible constants. If `NORMAL` is
     * passed, as in `textureMode(NORMAL)`, then the texture’s uv coordinates can
     * be provided in the range 0 to 1 instead of the image’s dimensions. This can
     * be helpful for using the same code for multiple images of different sizes.
     * For example, the code snippet above could be rewritten as follows:
     *
     * ```js
     * // Set the texture mode to use normalized coordinates.
     * textureMode(NORMAL);
     *
     * // Apply the image as a texture.
     * texture(img);
     *
     * // Draw the rectangle.
     * beginShape();
     *
     * // Top-left.
     * // u: 0, v: 0
     * vertex(0, 0, 0, 0, 0);
     *
     * // Top-right.
     * // u: 1, v: 0
     * vertex(30, 0, 0, 1, 0);
     *
     * // Bottom-right.
     * // u: 1, v: 1
     * vertex(30, 50, 0, 1, 1);
     *
     * // Bottom-left.
     * // u: 0, v: 1
     * vertex(0, 50, 0, 0, 1);
     *
     * endShape();
     * ```
     *
     * By default, `mode` is `IMAGE`, which scales uv coordinates to the
     * dimensions of the image. Calling `textureMode(IMAGE)` applies the default.
     *
     * Note: `textureMode()` can only be used in WebGL mode.
     *
     * @method  textureMode
     * @param {(IMAGE|NORMAL)} mode either IMAGE or NORMAL.
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load an image and create a p5.Image object.
     *   img = await loadImage('assets/laDefense.jpg');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('An image of a ceiling against a black background.');
     * }
     *
     * function draw() {
     *   background(0);
     *
     *   // Apply the image as a texture.
     *   texture(img);
     *
     *   // Draw the custom shape.
     *   // Use the image's width and height as uv coordinates.
     *   beginShape();
     *   vertex(-30, -30, 0, 0);
     *   vertex(30, -30, img.width, 0);
     *   vertex(30, 30, img.width, img.height);
     *   vertex(-30, 30, 0, img.height);
     *   endShape();
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   // Load an image and create a p5.Image object.
     *   img = await loadImage('assets/laDefense.jpg');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('An image of a ceiling against a black background.');
     * }
     *
     * function draw() {
     *   background(0);
     *
     *   // Set the texture mode.
     *   textureMode(NORMAL);
     *
     *   // Apply the image as a texture.
     *   texture(img);
     *
     *   // Draw the custom shape.
     *   // Use normalized uv coordinates.
     *   beginShape();
     *   vertex(-30, -30, 0, 0);
     *   vertex(30, -30, 1, 0);
     *   vertex(30, 30, 1, 1);
     *   vertex(-30, 30, 0, 1);
     *   endShape();
     * }
     */
    fn.textureMode = function (mode) {
      if (mode !== IMAGE && mode !== NORMAL) {
        console.warn(
          `You tried to set ${mode} textureMode only supports IMAGE & NORMAL `,
        );
      } else {
        this._renderer.states.setValue("textureMode", mode);
      }
    };

    /**
     * Changes the way textures behave when a shape’s uv coordinates go beyond the
     * texture.
     *
     * In order for <a href="#/p5/texture">texture()</a> to work, a shape needs a
     * way to map the points on its surface to the pixels in an image. Built-in
     * shapes such as <a href="#/p5/rect">rect()</a> and
     * <a href="#/p5/box">box()</a> already have these texture mappings based on
     * their vertices. Custom shapes created with
     * <a href="#/p5/vertex">vertex()</a> require texture mappings to be passed as
     * uv coordinates.
     *
     * Each call to <a href="#/p5/vertex">vertex()</a> must include 5 arguments,
     * as in `vertex(x, y, z, u, v)`, to map the vertex at coordinates `(x, y, z)`
     * to the pixel at coordinates `(u, v)` within an image. For example, the
     * corners of a rectangular image are mapped to the corners of a rectangle by default:
     *
     * ```js
     * // Apply the image as a texture.
     * texture(img);
     *
     * // Draw the rectangle.
     * rect(0, 0, 30, 50);
     * ```
     *
     * If the image in the code snippet above has dimensions of 300 x 500 pixels,
     * the same result could be achieved as follows:
     *
     * ```js
     * // Apply the image as a texture.
     * texture(img);
     *
     * // Draw the rectangle.
     * beginShape();
     *
     * // Top-left.
     * // u: 0, v: 0
     * vertex(0, 0, 0, 0, 0);
     *
     * // Top-right.
     * // u: 300, v: 0
     * vertex(30, 0, 0, 300, 0);
     *
     * // Bottom-right.
     * // u: 300, v: 500
     * vertex(30, 50, 0, 300, 500);
     *
     * // Bottom-left.
     * // u: 0, v: 500
     * vertex(0, 50, 0, 0, 500);
     *
     * endShape();
     * ```
     *
     * `textureWrap()` controls how textures behave when their uv's go beyond the
     * texture. Doing so can produce interesting visual effects such as tiling.
     * For example, the custom shape above could have u-coordinates are greater
     * than the image’s width:
     *
     * ```js
     * // Apply the image as a texture.
     * texture(img);
     *
     * // Draw the rectangle.
     * beginShape();
     * vertex(0, 0, 0, 0, 0);
     *
     * // Top-right.
     * // u: 600
     * vertex(30, 0, 0, 600, 0);
     *
     * // Bottom-right.
     * // u: 600
     * vertex(30, 50, 0, 600, 500);
     *
     * vertex(0, 50, 0, 0, 500);
     * endShape();
     * ```
     *
     * The u-coordinates of 600 are greater than the texture image’s width of 300.
     * This creates interesting possibilities.
     *
     * The first parameter, `wrapX`, accepts three possible constants. If `CLAMP`
     * is passed, as in `textureWrap(CLAMP)`, the pixels at the edge of the
     * texture will extend to the shape’s edges. If `REPEAT` is passed, as in
     * `textureWrap(REPEAT)`, the texture will tile repeatedly until reaching the
     * shape’s edges. If `MIRROR` is passed, as in `textureWrap(MIRROR)`, the
     * texture will tile repeatedly until reaching the shape’s edges, flipping
     * its orientation between tiles. By default, textures `CLAMP`.
     *
     * The second parameter, `wrapY`, is optional. It accepts the same three
     * constants, `CLAMP`, `REPEAT`, and `MIRROR`. If one of these constants is
     * passed, as in `textureWRAP(MIRROR, REPEAT)`, then the texture will `MIRROR`
     * horizontally and `REPEAT` vertically. By default, `wrapY` will be set to
     * the same value as `wrapX`.
     *
     * Note: `textureWrap()` can only be used in WebGL mode.
     *
     * @method textureWrap
     * @param {(CLAMP|REPEAT|MIRROR)} wrapX either CLAMP, REPEAT, or MIRROR
     * @param {(CLAMP|REPEAT|MIRROR)} [wrapY=wrapX] either CLAMP, REPEAT, or MIRROR
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   img = await loadImage('assets/rockies128.jpg');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     'An image of a landscape occupies the top-left corner of a square. Its edge colors smear to cover the other thre quarters of the square.'
     *   );
     * }
     *
     * function draw() {
     *   background(0);
     *
     *   // Set the texture mode.
     *   textureMode(NORMAL);
     *
     *   // Set the texture wrapping.
     *   // Note: CLAMP is the default mode.
     *   textureWrap(CLAMP);
     *
     *   // Apply the image as a texture.
     *   texture(img);
     *
     *   // Style the shape.
     *   noStroke();
     *
     *   // Draw the shape.
     *   // Use uv coordinates > 1.
     *   beginShape();
     *   vertex(-30, -30, 0, 0, 0);
     *   vertex(30, -30, 0, 2, 0);
     *   vertex(30, 30, 0, 2, 2);
     *   vertex(-30, 30, 0, 0, 2);
     *   endShape();
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   img = await loadImage('assets/rockies128.jpg');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('Four identical images of a landscape arranged in a grid.');
     * }
     *
     * function draw() {
     *   background(0);
     *
     *   // Set the texture mode.
     *   textureMode(NORMAL);
     *
     *   // Set the texture wrapping.
     *   textureWrap(REPEAT);
     *
     *   // Apply the image as a texture.
     *   texture(img);
     *
     *   // Style the shape.
     *   noStroke();
     *
     *   // Draw the shape.
     *   // Use uv coordinates > 1.
     *   beginShape();
     *   vertex(-30, -30, 0, 0, 0);
     *   vertex(30, -30, 0, 2, 0);
     *   vertex(30, 30, 0, 2, 2);
     *   vertex(-30, 30, 0, 0, 2);
     *   endShape();
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   img = await loadImage('assets/rockies128.jpg');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     'Four identical images of a landscape arranged in a grid. The images are reflected horizontally and vertically, creating a kaleidoscope effect.'
     *   );
     * }
     *
     * function draw() {
     *   background(0);
     *
     *   // Set the texture mode.
     *   textureMode(NORMAL);
     *
     *   // Set the texture wrapping.
     *   textureWrap(MIRROR);
     *
     *   // Apply the image as a texture.
     *   texture(img);
     *
     *   // Style the shape.
     *   noStroke();
     *
     *   // Draw the shape.
     *   // Use uv coordinates > 1.
     *   beginShape();
     *   vertex(-30, -30, 0, 0, 0);
     *   vertex(30, -30, 0, 2, 0);
     *   vertex(30, 30, 0, 2, 2);
     *   vertex(-30, 30, 0, 0, 2);
     *   endShape();
     * }
     *
     * @example
     * let img;
     *
     * async function setup() {
     *   img = await loadImage('assets/rockies128.jpg');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     'Four identical images of a landscape arranged in a grid. The top row and bottom row are reflections of each other.'
     *   );
     * }
     *
     * function draw() {
     *   background(0);
     *
     *   // Set the texture mode.
     *   textureMode(NORMAL);
     *
     *   // Set the texture wrapping.
     *   textureWrap(REPEAT, MIRROR);
     *
     *   // Apply the image as a texture.
     *   texture(img);
     *
     *   // Style the shape.
     *   noStroke();
     *
     *   // Draw the shape.
     *   // Use uv coordinates > 1.
     *   beginShape();
     *   vertex(-30, -30, 0, 0, 0);
     *   vertex(30, -30, 0, 2, 0);
     *   vertex(30, 30, 0, 2, 2);
     *   vertex(-30, 30, 0, 0, 2);
     *   endShape();
     * }
     */
    fn.textureWrap = function (wrapX, wrapY = wrapX) {
      this._renderer.states.setValue("textureWrapX", wrapX);
      this._renderer.states.setValue("textureWrapY", wrapY);

      for (const texture of this._renderer.textures.values()) {
        texture.setWrapMode(wrapX, wrapY);
      }
    };

    /**
     * Sets the current material as a normal material.
     *
     * A normal material sets surfaces facing the x-axis to red, those facing the
     * y-axis to green, and those facing the z-axis to blue. Normal material isn't
     * affected by light. It’s often used as a placeholder material when debugging.
     *
     * Note: `normalMaterial()` can only be used in WebGL mode.
     *
     * @method normalMaterial
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A multicolor torus drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Style the torus.
     *   normalMaterial();
     *
     *   // Draw the torus.
     *   torus(30);
     * }
     */
    fn.normalMaterial = function (...args) {
      this._assert3d("normalMaterial");
      // p5._validateParameters('normalMaterial', args);

      this._renderer.normalMaterial(...args);

      return this;
    };

    /**
     * Sets the ambient color of shapes’ surface material.
     *
     * The `ambientMaterial()` color sets the components of the
     * <a href="#/p5/ambientLight">ambientLight()</a> color that shapes will
     * reflect. For example, calling `ambientMaterial(255, 255, 0)` would cause a
     * shape to reflect red and green light, but not blue light.
     *
     * `ambientMaterial()` can be called three ways with different parameters to
     * set the material’s color.
     *
     * The first way to call `ambientMaterial()` has one parameter, `gray`.
     * Grayscale values between 0 and 255, as in `ambientMaterial(50)`, can be
     * passed to set the material’s color. Higher grayscale values make shapes
     * appear brighter.
     *
     * The second way to call `ambientMaterial()` has one parameter, `color`. A
     * <a href="#/p5.Color">p5.Color</a> object, an array of color values, or a
     * CSS color string, as in `ambientMaterial('magenta')`, can be passed to set
     * the material’s color.
     *
     * The third way to call `ambientMaterial()` has three parameters, `v1`, `v2`,
     * and `v3`. RGB, HSB, or HSL values, as in `ambientMaterial(255, 0, 0)`, can
     * be passed to set the material’s colors. Color values will be interpreted
     * using the current <a href="#/p5/colorMode">colorMode()</a>.
     *
     * Note: `ambientMaterial()` can only be used in WebGL mode.
     *
     * @method ambientMaterial
     * @param  {Number} v1  red or hue value in the current
     *                       <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number} v2  green or saturation value in the
     *                      current <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number} v3  blue, brightness, or lightness value in the
     *                      current <a href="#/p5/colorMode">colorMode()</a>.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A magenta cube drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on a magenta ambient light.
     *   ambientLight(255, 0, 255);
     *
     *   // Draw the box.
     *   box();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A purple cube drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on a magenta ambient light.
     *   ambientLight(255, 0, 255);
     *
     *   // Add a dark gray ambient material.
     *   ambientMaterial(150);
     *
     *   // Draw the box.
     *   box();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A red cube drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on a magenta ambient light.
     *   ambientLight(255, 0, 255);
     *
     *   // Add a yellow ambient material using RGB values.
     *   ambientMaterial(255, 255, 0);
     *
     *   // Draw the box.
     *   box();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A red cube drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on a magenta ambient light.
     *   ambientLight(255, 0, 255);
     *
     *   // Add a yellow ambient material using a p5.Color object.
     *   let c = color(255, 255, 0);
     *   ambientMaterial(c);
     *
     *   // Draw the box.
     *   box();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A red cube drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on a magenta ambient light.
     *   ambientLight(255, 0, 255);
     *
     *   // Add a yellow ambient material using a color string.
     *   ambientMaterial('yellow');
     *
     *   // Draw the box.
     *   box();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A yellow cube drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on a white ambient light.
     *   ambientLight(255, 255, 255);
     *
     *   // Add a yellow ambient material using a color string.
     *   ambientMaterial('yellow');
     *
     *   // Draw the box.
     *   box();
     * }
     */
    /**
     * @method ambientMaterial
     * @param  {Number} gray grayscale value between 0 (black) and 255 (white).
     * @chainable
     */
    /**
     * @method ambientMaterial
     * @param  {p5.Color|Number[]|String} color
     *           color as a <a href="#/p5.Color">p5.Color</a> object,
     *            an array of color values, or a CSS string.
     * @chainable
     */
    fn.ambientMaterial = function (v1, v2, v3) {
      this._assert3d("ambientMaterial");
      // p5._validateParameters('ambientMaterial', arguments);

      const color = fn.color.apply(this, arguments);
      this._renderer.states.setValue("_hasSetAmbient", true);
      this._renderer.states.setValue("curAmbientColor", color._array);
      this._renderer.states.setValue("_useNormalMaterial", false);
      this._renderer.states.setValue("enableLighting", true);
      if (!this._renderer.states.fillColor) {
        this._renderer.states.setValue("fillColor", new Color([1, 1, 1]));
      }
      return this;
    };

    /**
     * Sets the emissive color of shapes’ surface material.
     *
     * The `emissiveMaterial()` color sets a color shapes display at full
     * strength, regardless of lighting. This can give the appearance that a shape
     * is glowing. However, emissive materials don’t actually emit light that
     * can affect surrounding objects.
     *
     * `emissiveMaterial()` can be called three ways with different parameters to
     * set the material’s color.
     *
     * The first way to call `emissiveMaterial()` has one parameter, `gray`.
     * Grayscale values between 0 and 255, as in `emissiveMaterial(50)`, can be
     * passed to set the material’s color. Higher grayscale values make shapes
     * appear brighter.
     *
     * The second way to call `emissiveMaterial()` has one parameter, `color`. A
     * <a href="#/p5.Color">p5.Color</a> object, an array of color values, or a
     * CSS color string, as in `emissiveMaterial('magenta')`, can be passed to set
     * the material’s color.
     *
     * The third way to call `emissiveMaterial()` has four parameters, `v1`, `v2`,
     * `v3`, and `alpha`. `alpha` is optional. RGBA, HSBA, or HSLA values can be
     * passed to set the material’s colors, as in `emissiveMaterial(255, 0, 0)` or
     * `emissiveMaterial(255, 0, 0, 30)`. Color values will be interpreted using
     * the current <a href="#/p5/colorMode">colorMode()</a>.
     *
     * Note: `emissiveMaterial()` can only be used in WebGL mode.
     *
     * @method emissiveMaterial
     * @param  {Number} v1       red or hue value in the current
     *                           <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number} v2       green or saturation value in the
     *                           current <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number} v3       blue, brightness, or lightness value in the
     *                           current <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number} [alpha]  alpha value in the current
     *                           <a href="#/p5/colorMode">colorMode()</a>.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A red cube drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on a white ambient light.
     *   ambientLight(255, 255, 255);
     *
     *   // Add a red emissive material using RGB values.
     *   emissiveMaterial(255, 0, 0);
     *
     *   // Draw the box.
     *   box();
     * }
     */
    /**
     * @method emissiveMaterial
     * @param  {Number} gray grayscale value between 0 (black) and 255 (white).
     * @chainable
     */
    /**
     * @method emissiveMaterial
     * @param  {p5.Color|Number[]|String} color
     *           color as a <a href="#/p5.Color">p5.Color</a> object,
     *            an array of color values, or a CSS string.
     * @chainable
     */
    fn.emissiveMaterial = function (v1, v2, v3, a) {
      this._assert3d("emissiveMaterial");
      // p5._validateParameters('emissiveMaterial', arguments);

      const color = fn.color.apply(this, arguments);
      this._renderer.states.setValue("curEmissiveColor", color._array);
      this._renderer.states.setValue("_useEmissiveMaterial", true);
      this._renderer.states.setValue("_useNormalMaterial", false);
      this._renderer.states.setValue("enableLighting", true);

      return this;
    };

    /**
     * Sets the specular color of shapes’ surface material.
     *
     * The `specularMaterial()` color sets the components of light color that
     * glossy coats on shapes will reflect. For example, calling
     * `specularMaterial(255, 255, 0)` would cause a shape to reflect red and
     * green light, but not blue light.
     *
     * Unlike <a href="#/p5/ambientMaterial">ambientMaterial()</a>,
     * `specularMaterial()` will reflect the full color of light sources including
     * <a href="#/p5/directionalLight">directionalLight()</a>,
     * <a href="#/p5/pointLight">pointLight()</a>,
     * and <a href="#/p5/spotLight">spotLight()</a>. This is what gives it shapes
     * their "shiny" appearance. The material’s shininess can be controlled by the
     * <a href="#/p5/shininess">shininess()</a> function.
     *
     * `specularMaterial()` can be called three ways with different parameters to
     * set the material’s color.
     *
     * The first way to call `specularMaterial()` has one parameter, `gray`.
     * Grayscale values between 0 and 255, as in `specularMaterial(50)`, can be
     * passed to set the material’s color. Higher grayscale values make shapes
     * appear brighter.
     *
     * The second way to call `specularMaterial()` has one parameter, `color`. A
     * <a href="#/p5.Color">p5.Color> object, an array of color values, or a CSS
     * color string, as in `specularMaterial('magenta')`, can be passed to set the
     * material’s color.
     *
     * The third way to call `specularMaterial()` has four parameters, `v1`, `v2`,
     * `v3`, and `alpha`. `alpha` is optional. RGBA, HSBA, or HSLA values can be
     * passed to set the material’s colors, as in `specularMaterial(255, 0, 0)` or
     * `specularMaterial(255, 0, 0, 30)`. Color values will be interpreted using
     * the current <a href="#/p5/colorMode">colorMode()</a>.
     *
     * @method specularMaterial
     * @param  {Number} gray grayscale value between 0 (black) and 255 (white).
     * @param  {Number} [alpha] alpha value in the current current
     *                          <a href="#/p5/colorMode">colorMode()</a>.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     * // Double-click the canvas to apply a specular material.
     *
     * let isGlossy = false;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A red torus drawn on a gray background. It becomes glossy when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on a white point light at the top-right.
     *   pointLight(255, 255, 255, 30, -40, 30);
     *
     *   // Add a glossy coat if the user has double-clicked.
     *   if (isGlossy === true) {
     *     specularMaterial(255);
     *     shininess(50);
     *   }
     *
     *   // Style the torus.
     *   noStroke();
     *   fill(255, 0, 0);
     *
     *   // Draw the torus.
     *   torus(30);
     * }
     *
     * // Make the torus glossy when the user double-clicks.
     * function doubleClicked() {
     *   isGlossy = true;
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     * // Double-click the canvas to apply a specular material.
     *
     * let isGlossy = false;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     'A red torus drawn on a gray background. It becomes glossy and reflects green light when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on a white point light at the top-right.
     *   pointLight(255, 255, 255, 30, -40, 30);
     *
     *   // Add a glossy green coat if the user has double-clicked.
     *   if (isGlossy === true) {
     *     specularMaterial(0, 255, 0);
     *     shininess(50);
     *   }
     *
     *   // Style the torus.
     *   noStroke();
     *   fill(255, 0, 0);
     *
     *   // Draw the torus.
     *   torus(30);
     * }
     *
     * // Make the torus glossy when the user double-clicks.
     * function doubleClicked() {
     *   isGlossy = true;
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     * // Double-click the canvas to apply a specular material.
     *
     * let isGlossy = false;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     'A red torus drawn on a gray background. It becomes glossy and reflects green light when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on a white point light at the top-right.
     *   pointLight(255, 255, 255, 30, -40, 30);
     *
     *   // Add a glossy green coat if the user has double-clicked.
     *   if (isGlossy === true) {
     *     // Create a p5.Color object.
     *     let c = color('green');
     *     specularMaterial(c);
     *     shininess(50);
     *   }
     *
     *   // Style the torus.
     *   noStroke();
     *   fill(255, 0, 0);
     *
     *   // Draw the torus.
     *   torus(30);
     * }
     *
     * // Make the torus glossy when the user double-clicks.
     * function doubleClicked() {
     *   isGlossy = true;
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     * // Double-click the canvas to apply a specular material.
     *
     * let isGlossy = false;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     'A red torus drawn on a gray background. It becomes glossy and reflects green light when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on a white point light at the top-right.
     *   pointLight(255, 255, 255, 30, -40, 30);
     *
     *   // Add a glossy green coat if the user has double-clicked.
     *   if (isGlossy === true) {
     *     specularMaterial('#00FF00');
     *     shininess(50);
     *   }
     *
     *   // Style the torus.
     *   noStroke();
     *   fill(255, 0, 0);
     *
     *   // Draw the torus.
     *   torus(30);
     * }
     *
     * // Make the torus glossy when the user double-clicks.
     * function doubleClicked() {
     *   isGlossy = true;
     * }
     */
    /**
     * @method specularMaterial
     * @param  {Number}        v1      red or hue value in
     *                                 the current <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number}        v2      green or saturation value
     *                                 in the current <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number}        v3      blue, brightness, or lightness value
     *                                 in the current <a href="#/p5/colorMode">colorMode()</a>.
     * @param  {Number}        [alpha]
     * @chainable
     */
    /**
     * @method specularMaterial
     * @param  {p5.Color|Number[]|String} color
     *           color as a <a href="#/p5.Color">p5.Color</a> object,
     *            an array of color values, or a CSS string.
     * @chainable
     */
    fn.specularMaterial = function (v1, v2, v3, alpha) {
      this._assert3d("specularMaterial");
      // p5._validateParameters('specularMaterial', arguments);

      const color = fn.color.apply(this, arguments);
      this._renderer.states.setValue("curSpecularColor", color._array);
      this._renderer.states.setValue("_useSpecularMaterial", true);
      this._renderer.states.setValue("_useNormalMaterial", false);
      this._renderer.states.setValue("enableLighting", true);

      return this;
    };

    /**
     * Sets the amount of gloss ("shininess") of a
     * <a href="#/p5/specularMaterial">specularMaterial()</a>.
     *
     * Shiny materials focus reflected light more than dull materials.
     * `shininess()` affects the way materials reflect light sources including
     * <a href="#/p5/directionalLight">directionalLight()</a>,
     * <a href="#/p5/pointLight">pointLight()</a>,
     * and <a href="#/p5/spotLight">spotLight()</a>.
     *
     * The parameter, `shine`, is a number that sets the amount of shininess.
     * `shine` must be greater than 1, which is its default value.
     *
     * @method shininess
     * @param {Number} shine amount of shine.
     * @chainable
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     'Two red spheres drawn on a gray background. White light reflects from their surfaces as the mouse moves. The right sphere is shinier than the left sphere.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Turn on a red ambient light.
     *   ambientLight(255, 0, 0);
     *
     *   // Get the mouse's coordinates.
     *   let mx = mouseX - 50;
     *   let my = mouseY - 50;
     *
     *   // Turn on a white point light that follows the mouse.
     *   pointLight(255, 255, 255, mx, my, 50);
     *
     *   // Style the sphere.
     *   noStroke();
     *
     *   // Add a specular material with a grayscale value.
     *   specularMaterial(255);
     *
     *   // Draw the left sphere with low shininess.
     *   translate(-25, 0, 0);
     *   shininess(10);
     *   sphere(20);
     *
     *   // Draw the right sphere with high shininess.
     *   translate(50, 0, 0);
     *   shininess(100);
     *   sphere(20);
     * }
     */
    fn.shininess = function (shine) {
      this._assert3d("shininess");
      // p5._validateParameters('shininess', arguments);

      this._renderer.shininess(shine);

      return this;
    };

    /**
     * Sets the amount of "metalness" of a
     * <a href="#/p5/specularMaterial">specularMaterial()</a>.
     *
     * `metalness()` can make materials appear more metallic. It affects the way
     * materials reflect light sources including
     * affects the way materials reflect light sources including
     * <a href="#/p5/directionalLight">directionalLight()</a>,
     * <a href="#/p5/pointLight">pointLight()</a>,
     * <a href="#/p5/spotLight">spotLight()</a>, and
     * <a href="#/p5/imageLight">imageLight()</a>.
     *
     * The parameter, `metallic`, is a number that sets the amount of metalness.
     * `metallic` must be greater than 1, which is its default value. Higher
     * values, such as `metalness(100)`, make specular materials appear more
     * metallic.
     *
     * @method metalness
     * @param {Number} metallic amount of metalness.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     'Two blue spheres drawn on a gray background. White light reflects from their surfaces as the mouse moves. The right sphere is more metallic than the left sphere.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Turn on an ambient light.
     *   ambientLight(200);
     *
     *   // Get the mouse's coordinates.
     *   let mx = mouseX - 50;
     *   let my = mouseY - 50;
     *
     *   // Turn on a white point light that follows the mouse.
     *   pointLight(255, 255, 255, mx, my, 50);
     *
     *   // Style the spheres.
     *   noStroke();
     *   fill(30, 30, 255);
     *   specularMaterial(255);
     *   shininess(20);
     *
     *   // Draw the left sphere with low metalness.
     *   translate(-25, 0, 0);
     *   metalness(1);
     *   sphere(20);
     *
     *   // Draw the right sphere with high metalness.
     *   translate(50, 0, 0);
     *   metalness(50);
     *   sphere(20);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let img;
     *
     * async function setup() {
     *   img = await loadImage('assets/outdoor_spheremap.jpg');
     *
     *   createCanvas(100 ,100 ,WEBGL);
     *
     *   describe(
     *     'Two spheres floating above a landscape. The surface of the spheres reflect the landscape. The right sphere is more reflective than the left sphere.'
     *   );
     * }
     *
     * function draw() {
     *   // Add the panorama.
     *   panorama(img);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Use the image as a light source.
     *   imageLight(img);
     *
     *   // Style the spheres.
     *   noStroke();
     *   specularMaterial(50);
     *   shininess(200);
     *
     *   // Draw the left sphere with low metalness.
     *   translate(-25, 0, 0);
     *   metalness(1);
     *   sphere(20);
     *
     *   // Draw the right sphere with high metalness.
     *   translate(50, 0, 0);
     *   metalness(50);
     *   sphere(20);
     * }
     */
    fn.metalness = function (metallic) {
      this._assert3d("metalness");

      this._renderer.metalness(metallic);

      return this;
    };

    Renderer3D.prototype.shader = function (s) {
      // Always set the shader as a fill shader
      this.states.setValue("userFillShader", s);
      this.states.setValue("_useNormalMaterial", false);
      s.ensureCompiledOnContext(this);
      s.setDefaultUniforms();
    };

    Renderer3D.prototype.strokeShader = function (s) {
      this.states.setValue("userStrokeShader", s);
      s.ensureCompiledOnContext(this);
      s.setDefaultUniforms();
    };

    Renderer3D.prototype.imageShader = function (s) {
      this.states.setValue("userImageShader", s);
      s.ensureCompiledOnContext(this);
      s.setDefaultUniforms();
    };

    Renderer3D.prototype.resetShader = function () {
      this.states.setValue("userFillShader", null);
      this.states.setValue("userStrokeShader", null);
      this.states.setValue("userImageShader", null);
    };

    Renderer3D.prototype.texture = function (tex) {
      this.states.setValue("drawMode", TEXTURE);
      this.states.setValue("_useNormalMaterial", false);
      this.states.setValue("_tex", tex);
      this.states.setValue("fillColor", new Color([1, 1, 1]));
    };

    Renderer3D.prototype.normalMaterial = function (...args) {
      this.states.setValue("drawMode", FILL);
      this.states.setValue("_useSpecularMaterial", false);
      this.states.setValue("_useEmissiveMaterial", false);
      this.states.setValue("_useNormalMaterial", true);
      this.states.setValue("curFillColor", [1, 1, 1, 1]);
      this.states.setValue("fillColor", new Color([1, 1, 1]));
      this.states.setValue("strokeColor", null);
    };

    // Renderer3D.prototype.ambientMaterial = function(v1, v2, v3) {
    // }

    // Renderer3D.prototype.emissiveMaterial = function(v1, v2, v3, a) {
    // }

    // Renderer3D.prototype.specularMaterial = function(v1, v2, v3, alpha) {
    // }

    Renderer3D.prototype.shininess = function (shine) {
      if (shine < 1) {
        shine = 1;
      }
      this.states.setValue("_useShininess", shine);
    };

    Renderer3D.prototype.metalness = function (metallic) {
      const metalMix = 1 - Math.exp(-metallic / 100);
      this.states.setValue("_useMetalness", metalMix);
    };
  }

  if (typeof p5 !== "undefined") {
    loading(p5, p5.prototype);
  }

  /**
   * @module Math
   * @submodule Trigonometry
   * @for p5
   * @requires core
   * @requires constants
   */


  function trigonometry(p5, fn){
    /**
     * A `String` constant that's used to set the
     * <a href="#/p5/angleMode">angleMode()</a>.
     *
     * By default, functions such as <a href="#/p5/rotate">rotate()</a> and
     * <a href="#/p5/sin">sin()</a> expect angles measured in units of radians.
     * Calling `angleMode(DEGREES)` ensures that angles are measured in units of
     * degrees.
     *
     * Note: `TWO_PI` radians equals 360˚.
     *
     * @typedef {'degrees'} DEGREES
     * @property {DEGREES} DEGREES
     * @final
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Draw a red arc from 0 to HALF_PI radians.
     *   fill(255, 0, 0);
     *   arc(50, 50, 80, 80, 0, HALF_PI);
     *
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   // Draw a blue arc from 90˚ to 180˚.
     *   fill(0, 0, 255);
     *   arc(50, 50, 80, 80, 90, 180);
     *
     *   describe('The bottom half of a circle drawn on a gray background. The bottom-right quarter is red. The bottom-left quarter is blue.');
     * }
     */
    const DEGREES = fn.DEGREES = 'degrees';

    /**
     * A `String` constant that's used to set the
     * <a href="#/p5/angleMode">angleMode()</a>.
     *
     * By default, functions such as <a href="#/p5/rotate">rotate()</a> and
     * <a href="#/p5/sin">sin()</a> expect angles measured in units of radians.
     * Calling `angleMode(RADIANS)` ensures that angles are measured in units of
     * radians. Doing so can be useful if the
     * <a href="#/p5/angleMode">angleMode()</a> has been set to
     * <a href="#/p5/DEGREES">DEGREES</a>.
     *
     * Note: `TWO_PI` radians equals 360˚.
     *
     * @typedef {'radians'} RADIANS
     * @property {RADIANS} RADIANS
     * @final
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   // Draw a red arc from 0˚ to 90˚.
     *   fill(255, 0, 0);
     *   arc(50, 50, 80, 80, 0, 90);
     *
     *   // Use radians.
     *   angleMode(RADIANS);
     *
     *   // Draw a blue arc from HALF_PI to PI.
     *   fill(0, 0, 255);
     *   arc(50, 50, 80, 80, HALF_PI, PI);
     *
     *   describe('The bottom half of a circle drawn on a gray background. The bottom-right quarter is red. The bottom-left quarter is blue.');
     * }
     */
    const RADIANS = fn.RADIANS = 'radians';

    /*
     * all DEGREES/RADIANS conversion should be done in the p5 instance
     * if possible, using the p5._toRadians(), p5._fromRadians() methods.
     */
    fn._angleMode = RADIANS;

    /**
     * Calculates the arc cosine of a number.
     *
     * `acos()` is the inverse of <a href="#/p5/cos">cos()</a>. It expects
     * arguments in the range -1 to 1. By default, `acos()` returns values in the
     * range 0 to &pi; (about 3.14). If the
     * <a href="#/p5/angleMode">angleMode()</a> is `DEGREES`, then values are
     * returned in the range 0 to 180.
     *
     * @method acos
     * @param  {Number} value value whose arc cosine is to be returned.
     * @return {Number}       arc cosine of the given value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Calculate cos() and acos() values.
     *   let a = PI;
     *   let c = cos(a);
     *   let ac = acos(c);
     *
     *   // Display the values.
     *   text(`${round(a, 3)}`, 35, 25);
     *   text(`${round(c, 3)}`, 35, 50);
     *   text(`${round(ac, 3)}`, 35, 75);
     *
     *   describe('The numbers 3.142, -1, and 3.142 written on separate rows.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Calculate cos() and acos() values.
     *   let a = PI + QUARTER_PI;
     *   let c = cos(a);
     *   let ac = acos(c);
     *
     *   // Display the values.
     *   text(`${round(a, 3)}`, 35, 25);
     *   text(`${round(c, 3)}`, 35, 50);
     *   text(`${round(ac, 3)}`, 35, 75);
     *
     *   describe('The numbers 3.927, -0.707, and 2.356 written on separate rows.');
     * }
     */
    fn.acos = function(ratio) {
      return this._fromRadians(Math.acos(ratio));
    };

    /**
     * Calculates the arc sine of a number.
     *
     * `asin()` is the inverse of <a href="#/p5/sin">sin()</a>. It expects input
     * values in the range of -1 to 1. By default, `asin()` returns values in the
     * range -&pi; &divide; 2 (about -1.57) to &pi; &divide; 2 (about 1.57). If
     * the <a href="#/p5/angleMode">angleMode()</a> is `DEGREES` then values are
     * returned in the range -90 to 90.
     *
     * @method asin
     * @param  {Number} value value whose arc sine is to be returned.
     * @return {Number}       arc sine of the given value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Calculate sin() and asin() values.
     *   let a = PI / 3;
     *   let s = sin(a);
     *   let as = asin(s);
     *
     *   // Display the values.
     *   text(`${round(a, 3)}`, 35, 25);
     *   text(`${round(s, 3)}`, 35, 50);
     *   text(`${round(as, 3)}`, 35, 75);
     *
     *   describe('The numbers 1.047, 0.866, and 1.047 written on separate rows.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Calculate sin() and asin() values.
     *   let a = PI + PI / 3;
     *   let s = sin(a);
     *   let as = asin(s);
     *
     *   // Display the values.
     *   text(`${round(a, 3)}`, 35, 25);
     *   text(`${round(s, 3)}`, 35, 50);
     *   text(`${round(as, 3)}`, 35, 75);
     *
     *   describe('The numbers 4.189, -0.866, and -1.047 written on separate rows.');
     * }
     */
    fn.asin = function(ratio) {
      return this._fromRadians(Math.asin(ratio));
    };

    /**
     * Calculates the arc tangent of a number.
     *
     * `atan()` is the inverse of <a href="#/p5/tan">tan()</a>. It expects input
     * values in the range of -Infinity to Infinity. By default, `atan()` returns
     * values in the range -&pi; &divide; 2 (about -1.57) to &pi; &divide; 2
     * (about 1.57). If the <a href="#/p5/angleMode">angleMode()</a> is `DEGREES`
     * then values are returned in the range -90 to 90.
     *
     * @method atan
     * @param  {Number} value value whose arc tangent is to be returned.
     * @return {Number}       arc tangent of the given value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Calculate tan() and atan() values.
     *   let a = PI / 3;
     *   let t = tan(a);
     *   let at = atan(t);
     *
     *   // Display the values.
     *   text(`${round(a, 3)}`, 35, 25);
     *   text(`${round(t, 3)}`, 35, 50);
     *   text(`${round(at, 3)}`, 35, 75);
     *
     *   describe('The numbers 1.047, 1.732, and 1.047 written on separate rows.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Calculate tan() and atan() values.
     *   let a = PI + PI / 3;
     *   let t = tan(a);
     *   let at = atan(t);
     *
     *   // Display the values.
     *   text(`${round(a, 3)}`, 35, 25);
     *   text(`${round(t, 3)}`, 35, 50);
     *   text(`${round(at, 3)}`, 35, 75);
     *
     *   describe('The numbers 4.189, 1.732, and 1.047 written on separate rows.');
     * }
     */
    fn.atan = function(ratio) {
      return this._fromRadians(Math.atan(ratio));
    };

    /**
     * Calculates the angle formed by a point, the origin, and the positive
     * x-axis.
     *
     * `atan2()` is most often used for orienting geometry to the mouse's
     * position, as in `atan2(mouseY, mouseX)`. The first parameter is the point's
     * y-coordinate and the second parameter is its x-coordinate.
     *
     * By default, `atan2()` returns values in the range
     * -&pi; (about -3.14) to &pi; (3.14). If the
     * <a href="#/p5/angleMode">angleMode()</a> is `DEGREES`, then values are
     * returned in the range -180 to 180.
     *
     * @method atan2
     * @param  {Number} y y-coordinate of the point.
     * @param  {Number} x x-coordinate of the point.
     * @return {Number}   arc tangent of the given point.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A rectangle at the top-left of the canvas rotates with mouse movements.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the angle between the mouse
     *   // and the origin.
     *   let a = atan2(mouseY, mouseX);
     *
     *   // Rotate.
     *   rotate(a);
     *
     *   // Draw the shape.
     *   rect(0, 0, 60, 10);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A rectangle at the center of the canvas rotates with mouse movements.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Translate the origin to the center.
     *   translate(50, 50);
     *
     *   // Get the mouse's coordinates relative to the origin.
     *   let x = mouseX - 50;
     *   let y = mouseY - 50;
     *
     *   // Calculate the angle between the mouse and the origin.
     *   let a = atan2(y, x);
     *
     *   // Rotate.
     *   rotate(a);
     *
     *   // Draw the shape.
     *   rect(-30, -5, 60, 10);
     * }
     */
    fn.atan2 = function(y, x) {
      return this._fromRadians(Math.atan2(y, x));
    };

    /**
     * Calculates the cosine of an angle.
     *
     * `cos()` is useful for many geometric tasks in creative coding. The values
     * returned oscillate between -1 and 1 as the input angle increases. `cos()`
     * calculates the cosine of an angle, using radians by default, or according
     * to if <a href="#/p5/angleMode">angleMode()</a> setting (RADIANS or DEGREES).
     *
     * @method cos
     * @param  {Number} angle the angle, in radians by default, or according to if <a href="/reference/p5/angleMode/">angleMode()</a> setting (RADIANS or DEGREES).
     * @return {Number}       cosine of the angle.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A white ball on a string oscillates left and right.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the coordinates.
     *   let x = 30 * cos(frameCount * 0.05) + 50;
     *   let y = 50;
     *
     *   // Draw the oscillator.
     *   line(50, y, x, y);
     *   circle(x, y, 20);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   describe('A series of black dots form a wave pattern.');
     * }
     *
     * function draw() {
     *   // Calculate the coordinates.
     *   let x = frameCount;
     *   let y = 30 * cos(x * 0.1) + 50;
     *
     *   // Draw the point.
     *   point(x, y);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   describe('A series of black dots form an infinity symbol.');
     * }
     *
     * function draw() {
     *   // Calculate the coordinates.
     *   let x = 30 * cos(frameCount * 0.1) + 50;
     *   let y = 10 * sin(frameCount * 0.2) + 50;
     *
     *   // Draw the point.
     *   point(x, y);
     * }
     */
    fn.cos = function(angle) {
      return Math.cos(this._toRadians(angle));
    };

    /**
     * Calculates the sine of an angle.
     *
     * `sin()` is useful for many geometric tasks in creative coding. The values
     * returned oscillate between -1 and 1 as the input angle increases. `sin()`
     * calculates the sine of an angle, using radians by default, or according to
     * if <a href="#/p5/angleMode">angleMode()</a> setting (RADIANS or DEGREES).
     *
     * @method sin
     * @param  {Number} angle the angle, in radians by default, or according to if <a href="/reference/p5/angleMode/">angleMode()</a> setting (RADIANS or DEGREES).
     * @return {Number}       sine of the angle.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A white ball on a string oscillates up and down.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the coordinates.
     *   let x = 50;
     *   let y = 30 * sin(frameCount * 0.05) + 50;
     *
     *   // Draw the oscillator.
     *   line(50, y, x, y);
     *   circle(x, y, 20);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   describe('A series of black dots form a wave pattern.');
     * }
     *
     * function draw() {
     *   // Calculate the coordinates.
     *   let x = frameCount;
     *   let y = 30 * sin(x * 0.1) + 50;
     *
     *   // Draw the point.
     *   point(x, y);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   describe('A series of black dots form an infinity symbol.');
     * }
     *
     * function draw() {
     *   // Calculate the coordinates.
     *   let x = 30 * cos(frameCount * 0.1) + 50;
     *   let y = 10 * sin(frameCount * 0.2) + 50;
     *
     *   // Draw the point.
     *   point(x, y);
     * }
     */
    fn.sin = function(angle) {
      return Math.sin(this._toRadians(angle));
    };

    /**
     * Calculates the tangent of an angle.
     *
     * `tan()` is useful for many geometric tasks in creative coding. The values
     * returned range from -Infinity to Infinity and repeat periodically as the
     * input angle increases. `tan()` calculates the tan of an angle, using radians
     * by default, or according to
     * if <a href="#/p5/angleMode">angleMode()</a> setting (RADIANS or DEGREES).
     *
     * @method tan
     * @param  {Number} angle the angle, in radians by default, or according to if <a href="/reference/p5/angleMode/">angleMode()</a> setting (RADIANS or DEGREES).
     * @return {Number}       tangent of the angle.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   describe('A series of identical curves drawn with black dots. Each curve starts from the top of the canvas, continues down at a slight angle, flattens out at the middle of the canvas, then continues to the bottom.');
     * }
     *
     * function draw() {
     *   // Calculate the coordinates.
     *   let x = frameCount;
     *   let y = 5 * tan(x * 0.1) + 50;
     *
     *   // Draw the point.
     *   point(x, y);
     * }
     */
    fn.tan = function(angle) {
      return Math.tan(this._toRadians(angle));
    };

    /**
     * Converts an angle measured in radians to its value in degrees.
     *
     * Degrees and radians are both units for measuring angles. There are 360˚ in
     * one full rotation. A full rotation is 2 &times; &pi; (about 6.28) radians.
     *
     * The same angle can be expressed in with either unit. For example, 90° is a
     * quarter of a full rotation. The same angle is 2 &times; &pi; &divide; 4
     * (about 1.57) radians.
     *
     * @method degrees
     * @param  {Number} radians radians value to convert to degrees.
     * @return {Number}         converted angle.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Calculate the angle conversion.
     *   let rad = QUARTER_PI;
     *   let deg = degrees(rad);
     *
     *   // Display the conversion.
     *   text(`${round(rad, 2)} rad = ${deg}˚`, 10, 50);
     *
     *   describe('The text "0.79 rad = 45˚".');
     * }
     */
    fn.degrees = angle => angle * RAD_TO_DEG;

    /**
     * Converts an angle measured in degrees to its value in radians.
     *
     * Degrees and radians are both units for measuring angles. There are 360˚ in
     * one full rotation. A full rotation is 2 &times; &pi; (about 6.28) radians.
     *
     * The same angle can be expressed in with either unit. For example, 90° is a
     * quarter of a full rotation. The same angle is 2 &times; &pi; &divide; 4
     * (about 1.57) radians.
     *
     * @method radians
     * @param  {Number} degrees degree value to convert to radians.
     * @return {Number}         converted angle.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Caclulate the angle conversion.
     *   let deg = 45;
     *   let rad = radians(deg);
     *
     *   // Display the angle conversion.
     *   text(`${deg}˚ = ${round(rad, 3)} rad`, 10, 50);
     *
     *   describe('The text "45˚ = 0.785 rad".');
     * }
     */
    fn.radians = angle => angle * DEG_TO_RAD;

    /**
     * Changes the unit system used to measure angles.
     *
     * Degrees and radians are both units for measuring angles. There are 360˚ in
     * one full rotation. A full rotation is 2 &times; &pi; (about 6.28) radians.
     *
     * Functions such as <a href="#/p5/rotate">rotate()</a> and
     * <a href="#/p5/sin">sin()</a> expect angles measured radians by default.
     * Calling `angleMode(DEGREES)` switches to degrees. Calling
     * `angleMode(RADIANS)` switches back to radians.
     *
     * Calling `angleMode()` with no arguments returns current angle mode, which
     * is either `RADIANS` or `DEGREES`.
     *
     * @method angleMode
     * @param {(RADIANS|DEGREES)} mode either RADIANS or DEGREES.
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Rotate 1/8 turn.
     *   rotate(QUARTER_PI);
     *
     *   // Draw a line.
     *   line(0, 0, 80, 0);
     *
     *   describe('A diagonal line radiating from the top-left corner of a square.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   // Rotate 1/8 turn.
     *   rotate(45);
     *
     *   // Draw a line.
     *   line(0, 0, 80, 0);
     *
     *   describe('A diagonal line radiating from the top-left corner of a square.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Calculate the angle to rotate.
     *   let angle = TWO_PI / 7;
     *
     *   // Move the origin to the center.
     *   translate(50, 50);
     *
     *   // Style the flower.
     *   noStroke();
     *   fill(255, 50);
     *
     *   // Draw the flower.
     *   for (let i = 0; i < 7; i += 1) {
     *     ellipse(0, 0, 80, 20);
     *     rotate(angle);
     *   }
     *
     *   describe('A translucent white flower on a dark background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(50);
     *
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   // Calculate the angle to rotate.
     *   let angle = 360 / 7;
     *
     *   // Move the origin to the center.
     *   translate(50, 50);
     *
     *   // Style the flower.
     *   noStroke();
     *   fill(255, 50);
     *
     *   // Draw the flower.
     *   for (let i = 0; i < 7; i += 1) {
     *     ellipse(0, 0, 80, 20);
     *     rotate(angle);
     *   }
     *
     *   describe('A translucent white flower on a dark background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A white ball on a string oscillates left and right.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the coordinates.
     *   let x = 30 * cos(frameCount * 0.05) + 50;
     *   let y = 50;
     *
     *   // Draw the oscillator.
     *   line(50, y, x, y);
     *   circle(x, y, 20);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   describe('A white ball on a string oscillates left and right.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the coordinates.
     *   let x = 30 * cos(frameCount * 2.86) + 50;
     *   let y = 50;
     *
     *   // Draw the oscillator.
     *   line(50, y, x, y);
     *   circle(x, y, 20);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Draw the upper line.
     *   rotate(PI / 6);
     *   line(0, 0, 80, 0);
     *
     *   // Use degrees.
     *   angleMode(DEGREES);
     *
     *   // Draw the lower line.
     *   rotate(30);
     *   line(0, 0, 80, 0);
     *
     *   describe('Two diagonal lines radiating from the top-left corner of a square. The lines are oriented 30 degrees from the edges of the square and 30 degrees apart from each other.');
     * }
     */
    /**
     * @method angleMode
     * @return {(RADIANS|DEGREES)} mode either RADIANS or DEGREES
     */
    fn.angleMode = function(mode) {
      // p5._validateParameters('angleMode', arguments);
      if (typeof mode === 'undefined') {
        return this._angleMode;
      } else if (mode === DEGREES || mode === RADIANS) {
        const prevMode = this._angleMode;

        // No change
        if(mode === prevMode) return;

        // Otherwise adjust pRotation according to new mode
        // This is necessary for acceleration events to work properly
        if(mode === RADIANS) {
          // Change pRotation to radians
          this.pRotationX = this.pRotationX * DEG_TO_RAD;
          this.pRotationY = this.pRotationY * DEG_TO_RAD;
          this.pRotationZ = this.pRotationZ * DEG_TO_RAD;
        } else {
          // Change pRotation to degrees
          this.pRotationX = this.pRotationX * RAD_TO_DEG;
          this.pRotationY = this.pRotationY * RAD_TO_DEG;
          this.pRotationZ = this.pRotationZ * RAD_TO_DEG;
        }

        this._angleMode = mode;
      }
    };

    /**
     * converts angles from the current angleMode to RADIANS
     *
     * @method _toRadians
     * @private
     * @param {Number} angle
     * @returns {Number}
     */
    fn._toRadians = function(angle) {
      if (this._angleMode === DEGREES) {
        return angle * DEG_TO_RAD;
      }
      return angle;
    };

    /**
     * converts angles from the current angleMode to DEGREES
     *
     * @method _toDegrees
     * @private
     * @param {Number} angle
     * @returns {Number}
     */
    fn._toDegrees = function(angle) {
      if (this._angleMode === RADIANS) {
        return angle * RAD_TO_DEG;
      }
      return angle;
    };

    /**
     * converts angles from RADIANS into the current angleMode
     *
     * @method _fromRadians
     * @private
     * @param {Number} angle
     * @returns {Number}
     */
    fn._fromRadians = function(angle) {
      if (this._angleMode === DEGREES) {
        return angle * RAD_TO_DEG;
      }
      return angle;
    };

    /**
     * converts angles from DEGREES into the current angleMode
     *
     * @method _fromDegrees
     * @private
     * @param {Number} angle
     * @returns {Number}
     */
    fn._fromDegrees = function(angle) {
      if (this._angleMode === RADIANS) {
        return angle * DEG_TO_RAD;
      }
      return angle;
    };
  }

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

  /**
   * @module Rendering
   * @submodule Rendering
   * @for p5
   */


  class Graphics {
    constructor(w, h, renderer, pInst, canvas) {
      const r = renderer || P2D;

      this._pInst = pInst;
      this._renderer = new renderers[r](this, w, h, false, canvas);

      this._initializeInstanceVariables(this);

      this._renderer._applyDefaults();
      return this;
    }

    // This is to correctly extend the p5.Element interface
    get elt() {
      return this.canvas;
    }

    get deltaTime(){
      return this._pInst.deltaTime;
    }

    get canvas(){
      return this._renderer?.canvas;
    }

    get drawingContext(){
      return this._renderer.drawingContext;
    }

    get width(){
      return this._renderer?.width;
    }

    get height(){
      return this._renderer?.height;
    }

    get pixels(){
      return this._renderer?.pixels;
    }

    pixelDensity(val){
      let returnValue;
      if (typeof val === 'number') {
        if (val !== this._renderer._pixelDensity) {
          this._renderer._pixelDensity = val;
        }
        returnValue = this;
        this.resizeCanvas(this.width, this.height, true); // as a side effect, it will clear the canvas
      } else {
        returnValue = this._renderer._pixelDensity;
      }
      return returnValue;
    }

    resizeCanvas(w, h){
      this._renderer.resize(w, h);
    }

    /**
     * Resets the graphics buffer's transformations and lighting.
     *
     * By default, the main canvas resets certain transformation and lighting
     * values each time <a href="#/p5/draw">draw()</a> executes. `p5.Graphics`
     * objects must reset these values manually by calling `myGraphics.reset()`.
     *
     * @example
     * let pg;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.Graphics object.
     *   pg = createGraphics(60, 60);
     *
     *   describe('A white circle moves downward slowly within a dark square. The circle resets at the top of the dark square when the user presses the mouse.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Translate the p5.Graphics object's coordinate system.
     *   // The translation accumulates; the white circle moves.
     *   pg.translate(0, 0.1);
     *
     *   // Draw to the p5.Graphics object.
     *   pg.background(100);
     *   pg.circle(30, 0, 10);
     *
     *   // Display the p5.Graphics object.
     *   image(pg, 20, 20);
     *
     *   // Translate the main canvas' coordinate system.
     *   // The translation doesn't accumulate; the dark
     *   // square is always in the same place.
     *   translate(0, 0.1);
     *
     *   // Reset the p5.Graphics object when the
     *   // user presses the mouse.
     *   if (mouseIsPressed === true) {
     *     pg.reset();
     *   }
     * }
     *
     * @example
     * let pg;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.Graphics object.
     *   pg = createGraphics(60, 60);
     *
     *   describe('A white circle at the center of a dark gray square. The image is drawn on a light gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Translate the p5.Graphics object's coordinate system.
     *   pg.translate(30, 30);
     *
     *   // Draw to the p5.Graphics object.
     *   pg.background(100);
     *   pg.circle(0, 0, 10);
     *
     *   // Display the p5.Graphics object.
     *   image(pg, 20, 20);
     *
     *   // Reset the p5.Graphics object automatically.
     *   pg.reset();
     * }
     *
     * @example
     * let pg;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.Graphics object using WebGL mode.
     *   pg = createGraphics(100, 100, WEBGL);
     *
     *   describe("A sphere lit from above with a red light. The sphere's surface becomes glossy while the user clicks and holds the mouse.");
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Add a red point light from the top-right.
     *   pg.pointLight(255, 0, 0, 50, -100, 50);
     *
     *   // Style the sphere.
     *   // It should appear glossy when the
     *   // lighting values are reset.
     *   pg.noStroke();
     *   pg.specularMaterial(255);
     *   pg.shininess(100);
     *
     *   // Draw the sphere.
     *   pg.sphere(30);
     *
     *   // Display the p5.Graphics object.
     *   image(pg, -50, -50);
     *
     *   // Reset the p5.Graphics object when
     *   // the user presses the mouse.
     *   if (mouseIsPressed === true) {
     *     pg.reset();
     *   }
     * }
     *
     * @example
     * let pg;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.Graphics object using WebGL mode.
     *   pg = createGraphics(100, 100, WEBGL);
     *
     *   describe('A sphere with a glossy surface is lit from the top-right by a red light.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Add a red point light from the top-right.
     *   pg.pointLight(255, 0, 0, 50, -100, 50);
     *
     *   // Style the sphere.
     *   pg.noStroke();
     *   pg.specularMaterial(255);
     *   pg.shininess(100);
     *
     *   // Draw the sphere.
     *   pg.sphere(30);
     *
     *   // Display the p5.Graphics object.
     *   image(pg, 0, 0);
     *
     *   // Reset the p5.Graphics object automatically.
     *   pg.reset();
     * }
     */
    reset() {
      this._renderer.resetMatrix();
      if (this._renderer.isP3D) {
        this._renderer._update();
      }
    }

    /**
     * Removes the graphics buffer from the web page.
     *
     * Calling `myGraphics.remove()` removes the graphics buffer's
     * `&lt;canvas&gt;` element from the web page. The graphics buffer also uses
     * a bit of memory on the CPU that can be freed like so:
     *
     * ```js
     * // Remove the graphics buffer from the web page.
     * myGraphics.remove();
     *
     * // Delete the graphics buffer from CPU memory.
     * myGraphics = undefined;
     * ```
     *
     * Note: All variables that reference the graphics buffer must be assigned
     * the value `undefined` to delete the graphics buffer from CPU memory. If any
     * variable still refers to the graphics buffer, then it won't be garbage
     * collected.
     *
     * @example
     * // Double-click to remove the p5.Graphics object.
     *
     * let pg;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.Graphics object.
     *   pg = createGraphics(60, 60);
     *
     *   // Draw to the p5.Graphics object.
     *   pg.background(100);
     *   pg.circle(30, 30, 20);
     *
     *   describe('A white circle at the center of a dark gray square disappears when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Display the p5.Graphics object if
     *   // it's available.
     *   if (pg) {
     *     image(pg, 20, 20);
     *   }
     * }
     *
     * // Remove the p5.Graphics object when the
     * // the user double-clicks.
     * function doubleClicked() {
     *   // Remove the p5.Graphics object from the web page.
     *   pg.remove();
     *   pg = undefined;
     * }
     */
    remove() {
      this._renderer.remove();
      this._renderer = undefined;
    }

    /**
     * Creates a new <a href="#/p5.Framebuffer">p5.Framebuffer</a> object with
     * the same WebGL context as the graphics buffer.
     *
     * <a href="#/p5.Framebuffer">p5.Framebuffer</a> objects are separate drawing
     * surfaces that can be used as textures in WebGL mode. They're similar to
     * <a href="#/p5.Graphics">p5.Graphics</a> objects and generally run much
     * faster when used as textures. Creating a
     * <a href="#/p5.Framebuffer">p5.Framebuffer</a> object in the same context
     * as the graphics buffer makes this speedup possible.
     *
     * The parameter, `options`, is optional. An object can be passed to configure
     * the <a href="#/p5.Framebuffer">p5.Framebuffer</a> object. The available
     * properties are:
     *
     * - `format`: data format of the texture, either `UNSIGNED_BYTE`, `FLOAT`, or `HALF_FLOAT`. Default is `UNSIGNED_BYTE`.
     * - `channels`: whether to store `RGB` or `RGBA` color channels. Default is to match the graphics buffer which is `RGBA`.
     * - `depth`: whether to include a depth buffer. Default is `true`.
     * - `depthFormat`: data format of depth information, either `UNSIGNED_INT` or `FLOAT`. Default is `FLOAT`.
     * - `stencil`: whether to include a stencil buffer for masking. `depth` must be `true` for this feature to work. Defaults to the value of `depth` which is `true`.
     * - `antialias`: whether to perform anti-aliasing. If set to `true`, as in `{ antialias: true }`, 2 samples will be used by default. The number of samples can also be set, as in `{ antialias: 4 }`. Default is to match <a href="#/p5/setAttributes">setAttributes()</a> which is `false` (`true` in Safari).
     * - `width`: width of the <a href="#/p5.Framebuffer">p5.Framebuffer</a> object. Default is to always match the graphics buffer width.
     * - `height`: height of the <a href="#/p5.Framebuffer">p5.Framebuffer</a> object. Default is to always match the graphics buffer height.
     * - `density`: pixel density of the <a href="#/p5.Framebuffer">p5.Framebuffer</a> object. Default is to always match the graphics buffer pixel density.
     * - `textureFiltering`: how to read values from the <a href="#/p5.Framebuffer">p5.Framebuffer</a> object. Either `LINEAR` (nearby pixels will be interpolated) or `NEAREST` (no interpolation). Generally, use `LINEAR` when using the texture as an image and `NEAREST` if reading the texture as data. Default is `LINEAR`.
     *
     * If the `width`, `height`, or `density` attributes are set, they won't
     * automatically match the graphics buffer and must be changed manually.
     *
     * @param {Object} [options] configuration options.
     * @param {UNSIGNED_BYTE|FLOAT|HALF_FLOAT} [options.format=UNSIGNED_BYTE] The data format of the texture.
     * @param {RGB|RGBA} [options.channels=RGBA] What color channels to include in the texture.
     * @param {Boolean} [options.depth=true] Whether to store depth information in the framebuffer.
     * @param {UNSIGNED_INT|FLOAT} [options.depthFormat=FLOAT] The format to store depth values in.
     * @param {Boolean} [options.stencil=true] Whether to include a stencil buffer (required for clipping.)
     * @param {Boolean|Number} [options.antialias] Whether to antialias when drawing to this framebuffer. Either a boolean, or the number of antialias samples to use.
     * @param {Number} [options.width] The width of the framebuffer. By default, it will match the main canvas.
     * @param {Number} [options.height] The height of the framebuffer. By default, it will match the main canvas.
     * @param {Number} [options.density] The pixel density of the framebuffer. By default, it will match the main canvas.
     * @param {LINEAR|NEAREST} [options.textureFiltering=LINEAR] The strategy used when reading values in the framebuffer in between pixels.
     * @return {p5.Framebuffer} new framebuffer.
     *
     * @example
     * // Click and hold a mouse button to change shapes.
     *
     * let pg;
     * let torusLayer;
     * let boxLayer;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.Graphics object using WebGL mode.
     *   pg = createGraphics(100, 100, WEBGL);
     *
     *   // Create the p5.Framebuffer objects.
     *   torusLayer = pg.createFramebuffer();
     *   boxLayer = pg.createFramebuffer();
     *
     *   describe('A grid of white toruses rotating against a dark gray background. The shapes become boxes while the user holds a mouse button.');
     * }
     *
     * function draw() {
     *   // Update and draw the layers offscreen.
     *   drawTorus();
     *   drawBox();
     *
     *   // Choose the layer to display.
     *   let layer;
     *   if (mouseIsPressed === true) {
     *     layer = boxLayer;
     *   } else {
     *     layer = torusLayer;
     *   }
     *
     *   // Draw to the p5.Graphics object.
     *   pg.background(50);
     *
     *   // Iterate from left to right.
     *   for (let x = -50; x < 50; x += 25) {
     *     // Iterate from top to bottom.
     *     for (let y = -50; y < 50; y += 25) {
     *       // Draw the layer to the p5.Graphics object
     *       pg.image(layer, x, y, 25, 25);
     *     }
     *   }
     *
     *   // Display the p5.Graphics object.
     *   image(pg, 0, 0);
     * }
     *
     * // Update and draw the torus layer offscreen.
     * function drawTorus() {
     *   // Start drawing to the torus p5.Framebuffer.
     *   torusLayer.begin();
     *
     *   // Clear the drawing surface.
     *   pg.clear();
     *
     *   // Turn on the lights.
     *   pg.lights();
     *
     *   // Rotate the coordinate system.
     *   pg.rotateX(frameCount * 0.01);
     *   pg.rotateY(frameCount * 0.01);
     *
     *   // Style the torus.
     *   pg.noStroke();
     *
     *   // Draw the torus.
     *   pg.torus(20);
     *
     *   // Start drawing to the torus p5.Framebuffer.
     *   torusLayer.end();
     * }
     *
     * // Update and draw the box layer offscreen.
     * function drawBox() {
     *   // Start drawing to the box p5.Framebuffer.
     *   boxLayer.begin();
     *
     *   // Clear the drawing surface.
     *   pg.clear();
     *
     *   // Turn on the lights.
     *   pg.lights();
     *
     *   // Rotate the coordinate system.
     *   pg.rotateX(frameCount * 0.01);
     *   pg.rotateY(frameCount * 0.01);
     *
     *   // Style the box.
     *   pg.noStroke();
     *
     *   // Draw the box.
     *   pg.box(30);
     *
     *   // Start drawing to the box p5.Framebuffer.
     *   boxLayer.end();
     * }
     *
     * @example
     * // Click and hold a mouse button to change shapes.
     *
     * let pg;
     * let torusLayer;
     * let boxLayer;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create an options object.
     *   let options = { width: 25, height: 25 };
     *
     *   // Create a p5.Graphics object using WebGL mode.
     *   pg = createGraphics(100, 100, WEBGL);
     *
     *   // Create the p5.Framebuffer objects.
     *   // Use options for configuration.
     *   torusLayer = pg.createFramebuffer(options);
     *   boxLayer = pg.createFramebuffer(options);
     *
     *   describe('A grid of white toruses rotating against a dark gray background. The shapes become boxes while the user holds a mouse button.');
     * }
     *
     * function draw() {
     *   // Update and draw the layers offscreen.
     *   drawTorus();
     *   drawBox();
     *
     *   // Choose the layer to display.
     *   let layer;
     *   if (mouseIsPressed === true) {
     *     layer = boxLayer;
     *   } else {
     *     layer = torusLayer;
     *   }
     *
     *   // Draw to the p5.Graphics object.
     *   pg.background(50);
     *
     *   // Iterate from left to right.
     *   for (let x = -50; x < 50; x += 25) {
     *     // Iterate from top to bottom.
     *     for (let y = -50; y < 50; y += 25) {
     *       // Draw the layer to the p5.Graphics object
     *       pg.image(layer, x, y);
     *     }
     *   }
     *
     *   // Display the p5.Graphics object.
     *   image(pg, 0, 0);
     * }
     *
     * // Update and draw the torus layer offscreen.
     * function drawTorus() {
     *   // Start drawing to the torus p5.Framebuffer.
     *   torusLayer.begin();
     *
     *   // Clear the drawing surface.
     *   pg.clear();
     *
     *   // Turn on the lights.
     *   pg.lights();
     *
     *   // Rotate the coordinate system.
     *   pg.rotateX(frameCount * 0.01);
     *   pg.rotateY(frameCount * 0.01);
     *
     *   // Style the torus.
     *   pg.noStroke();
     *
     *   // Draw the torus.
     *   pg.torus(5, 2.5);
     *
     *   // Start drawing to the torus p5.Framebuffer.
     *   torusLayer.end();
     * }
     *
     * // Update and draw the box layer offscreen.
     * function drawBox() {
     *   // Start drawing to the box p5.Framebuffer.
     *   boxLayer.begin();
     *
     *   // Clear the drawing surface.
     *   pg.clear();
     *
     *   // Turn on the lights.
     *   pg.lights();
     *
     *   // Rotate the coordinate system.
     *   pg.rotateX(frameCount * 0.01);
     *   pg.rotateY(frameCount * 0.01);
     *
     *   // Style the box.
     *   pg.noStroke();
     *
     *   // Draw the box.
     *   pg.box(7.5);
     *
     *   // Start drawing to the box p5.Framebuffer.
     *   boxLayer.end();
     * }
     */
    createFramebuffer(options) {
      return new Framebuffer$1(this._renderer, options);
    }

    _assert3d(name) {
      if (!this._renderer.isP3D)
        throw new Error(
          `${name}() is only supported in WEBGL mode. If you'd like to use 3D graphics and WebGL, see  https://p5js.org/examples/form-3d-primitives.html for more information.`
        );
    };

    _initializeInstanceVariables() {
      this._accessibleOutputs = {
        text: false,
        grid: false,
        textLabel: false,
        gridLabel: false
      };

      this._styles = [];

      // this._colorMode = RGB;
      // this._colorMaxes = {
      //   rgb: [255, 255, 255, 255],
      //   hsb: [360, 100, 100, 1],
      //   hsl: [360, 100, 100, 1]
      // };

      this._downKeys = {}; //Holds the key codes of currently pressed keys
    }
  }
  function graphics(p5, fn){
    /**
     * A class to describe a drawing surface that's separate from the main canvas.
     *
     * Each `p5.Graphics` object provides a dedicated drawing surface called a
     * *graphics buffer*. Graphics buffers are helpful when drawing should happen
     * offscreen. For example, separate scenes can be drawn offscreen and
     * displayed only when needed.
     *
     * `p5.Graphics` objects have nearly all the drawing features of the main
     * canvas. For example, calling the method `myGraphics.circle(50, 50, 20)`
     * draws to the graphics buffer. The resulting image can be displayed on the
     * main canvas by passing the `p5.Graphics` object to the
     * <a href="#/p5/image">image()</a> function, as in `image(myGraphics, 0, 0)`.
     *
     * Note: <a href="#/p5/createGraphics">createGraphics()</a> is the recommended
     * way to create an instance of this class.
     *
     * @class p5.Graphics
     * @extends p5.Element
     * @param {Number} w            width width of the graphics buffer in pixels.
     * @param {Number} h            height height of the graphics buffer in pixels.
     * @param {(P2D|WEBGL|P2DHDR)} renderer   the renderer to use, either P2D or WEBGL.
     * @param {p5} [pInst]          sketch instance.
     * @param {HTMLCanvasElement} [canvas]     existing `&lt;canvas&gt;` element to use.
     *
     * @example
     * let pg;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.Graphics object.
     *   pg = createGraphics(50, 50);
     *
     *   // Draw to the p5.Graphics object.
     *   pg.background(100);
     *   pg.circle(25, 25, 20);
     *
     *   describe('A dark gray square with a white circle at its center drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Display the p5.Graphics object.
     *   image(pg, 25, 25);
     * }
     *
     * @example
     * // Click the canvas to display the graphics buffer.
     *
     * let pg;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.Graphics object.
     *   pg = createGraphics(50, 50);
     *
     *   describe('A square appears on a gray background when the user presses the mouse. The square cycles between white and black.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the background color.
     *   let bg = frameCount % 255;
     *
     *   // Draw to the p5.Graphics object.
     *   pg.background(bg);
     *
     *   // Display the p5.Graphics object while
     *   // the user presses the mouse.
     *   if (mouseIsPressed === true) {
     *     image(pg, 25, 25);
     *   }
     * }
     */
    p5.Graphics = Graphics;

    // Shapes
    primitives(p5, p5.Graphics.prototype);
    attributes(p5, p5.Graphics.prototype);
    curves(p5, p5.Graphics.prototype);
    vertex(p5, p5.Graphics.prototype);
    customShapes(p5, p5.Graphics.prototype);

    setting(p5, p5.Graphics.prototype);
    loadingDisplaying(p5, p5.Graphics.prototype);
    image$1(p5, p5.Graphics.prototype);
    pixels(p5, p5.Graphics.prototype);

    transform$1(p5, p5.Graphics.prototype);

    primitives3D(p5, p5.Graphics.prototype);
    light(p5, p5.Graphics.prototype);
    material(p5, p5.Graphics.prototype);
    creatingReading(p5, p5.Graphics.prototype);
    trigonometry(p5, p5.Graphics.prototype);
  }

  /**
   * This module defines the p5.Texture class
   * @module 3D
   * @submodule Material
   * @for p5
   * @requires core
   */


  class Texture {
    constructor (renderer, obj, settings = {}) {
      this._renderer = renderer;

      this.src = obj;

      this.format = settings.format || 'rgba8unorm';
      this.minFilter = settings.minFilter || LINEAR;
      this.magFilter = settings.magFilter || LINEAR;
      this.wrapS = settings.wrapS || renderer.states.textureWrapX;
      this.wrapT = settings.wrapT || renderer.states.textureWrapY;
      this.dataType = settings.dataType || 'uint8';

      this.textureHandle = null;

      this._detectSourceType();

      const textureData = this._getTextureDataFromSource();
      this.width = textureData.width;
      this.height = textureData.height;

      this.init(textureData);
    }
    /*
      const support = checkWebGLCapabilities(renderer);
      if (this.glFormat === gl.HALF_FLOAT && !support.halfFloat) {
        console.log('This device does not support dataType HALF_FLOAT. Falling back to FLOAT.');
        this.glDataType = gl.FLOAT;
      }
      if (
        this.glFormat === gl.HALF_FLOAT &&
        (this.glMinFilter === gl.LINEAR || this.glMagFilter === gl.LINEAR) &&
        !support.halfFloatLinear
      ) {
        console.log('This device does not support linear filtering for dataType FLOAT. Falling back to NEAREST.');
        if (this.glMinFilter === gl.LINEAR) this.glMinFilter = gl.NEAREST;
        if (this.glMagFilter === gl.LINEAR) this.glMagFilter = gl.NEAREST;
      }
      if (this.glFormat === gl.FLOAT && !support.float) {
        console.log('This device does not support dataType FLOAT. Falling back to UNSIGNED_BYTE.');
        this.glDataType = gl.UNSIGNED_BYTE;
      }
      if (
        this.glFormat === gl.FLOAT &&
        (this.glMinFilter === gl.LINEAR || this.glMagFilter === gl.LINEAR) &&
        !support.floatLinear
      ) {
        console.log('This device does not support linear filtering for dataType FLOAT. Falling back to NEAREST.');
        if (this.glMinFilter === gl.LINEAR) this.glMinFilter = gl.NEAREST;
        if (this.glMagFilter === gl.LINEAR) this.glMagFilter = gl.NEAREST;
      }
    }*/

    _detectSourceType() {
      const obj = this.src;
      this.isFramebufferTexture = obj instanceof FramebufferTexture;
      this.isSrcP5Image = obj instanceof Image;
      this.isSrcP5Graphics = obj instanceof Graphics;
      this.isSrcP5Renderer = obj instanceof Renderer;
      this.isImageData = typeof ImageData !== 'undefined' && obj instanceof ImageData;
      this.isSrcMediaElement =
        typeof MediaElement !== 'undefined' && obj instanceof MediaElement;
      this.isSrcHTMLElement =
        typeof Element !== 'undefined' &&
        obj instanceof Element &&
        !this.isSrcMediaElement &&
        !this.isSrcP5Graphics &&
        !this.isSrcP5Renderer;
    }

    remove() {
      if (this.textureHandle) {
        this._renderer.deleteTexture(this.textureHandle);
        this.textureHandle = null;
      }
    }

    _getTextureDataFromSource () {
      let textureData;
      if (this.isFramebufferTexture) {
        textureData = this.src.rawTexture();
      } else if (this.isSrcP5Image) {
      // param is a p5.Image
        textureData = this.src.canvas;
      } else if (
        this.isSrcMediaElement ||
        this.isSrcHTMLElement
      ) {
        // if param is a video HTML element
        if (this.src._ensureCanvas) {
          this.src._ensureCanvas();
        }
        textureData = this.src.elt;
      } else if (this.isSrcP5Graphics || this.isSrcP5Renderer) {
        textureData = this.src.canvas;
      } else if (this.isImageData) {
        textureData = this.src;
      }
      return textureData;
    }

    /**
     * Initializes common texture parameters, creates a gl texture,
     * tries to upload the texture for the first time if data is
     * already available.
     */
    init(textureData) {
      if (!this.isFramebufferTexture) {
        this.textureHandle = this._renderer.createTexture({
          format: this.format,
          dataType: this.dataType,
          width: textureData.width,
          height: textureData.height,
        });
      } else {
        this.textureHandle = this._renderer.createFramebufferTextureHandle(this.src);
      }

      this._renderer.setTextureParams(this, {
        minFilter: this.minFilter,
        magFilter: this.magFilter,
        wrapS: this.wrapS,
        wrapT: this.wrapT
      });

      this.bindTexture();

      if (this._shouldDeferUpload()) {
        this._renderer.uploadTextureFromData(
          this.textureHandle,
          new Uint8Array(1, 1, 1, 1),
          1,
          1
        );
      } else if (!this.isFramebufferTexture) {
        // this.update()
        this._renderer.uploadTextureFromSource(
          this.textureHandle,
          textureData
        );
      }

      this.unbindTexture();
    }

    _shouldDeferUpload() {
      return (
        this.width === 0 ||
        this.height === 0 ||
        (this.isSrcMediaElement && !this.src.loadedmetadata)
      );
    }

    /**
     * Checks if the source data for this texture has changed (if it's
     * easy to do so) and reuploads the texture if necessary. If it's not
     * possible or to expensive to do a calculation to determine wheter or
     * not the data has occurred, this method simply re-uploads the texture.
     */
    update() {
      const textureData = this._getTextureDataFromSource();
      if (!textureData) return false;

      let updated = false;

      if (this._shouldUpdate(textureData)) {
        this.bindTexture();
        this._renderer.uploadTextureFromSource(this.textureHandle, textureData);
        updated = true;
      }

      return updated;
    }

    _shouldUpdate(textureData) {
      const data = this.src;
      if (data.width === 0 || data.height === 0) {
        return false; // nothing to do!
      }

      // FramebufferTexture instances wrap raw WebGL textures already, which
      // don't need any extra updating, as they already live on the GPU
      if (this.isFramebufferTexture) {
        this.src.update();
        return false;
      }

      let updated = false;
      // pull texture from data, make sure width & height are appropriate
      if (
        textureData.width !== this.width ||
        textureData.height !== this.height
      ) {
        updated = true;

        // make sure that if the width and height of this.src have changed
        // for some reason, we update our metadata and upload the texture again
        this.width = textureData.width || data.width;
        this.height = textureData.height || data.height;

        if (this.isSrcP5Image) {
          data.setModified(false);
        } else if (this.isSrcMediaElement || this.isSrcHTMLElement) {
          // on the first frame the metadata comes in, the size will be changed
          // from 0 to actual size, but pixels may not be available.
          // flag for update in a future frame.
          // if we don't do this, a paused video, for example, may not
          // send the first frame to texture memory.
          data.setModified && data.setModified(true);
        }
      } else if (this.isSrcP5Image) {
        if (data.gifProperties) {
          data._animateGif(this._renderer._pInst);
        }
        // for an image, we only update if the modified field has been set,
        // for example, by a call to p5.Image.set
        if (data.isModified()) {
          updated = true;
          data.setModified(false);
        }
      } else if (this.isSrcMediaElement) {
        // for a media element (video), we'll check if the current time in
        // the video frame matches the last time. if it doesn't match, the
        // video has advanced or otherwise been taken to a new frame,
        // and we need to upload it.
        if (data.isModified()) {
          // p5.MediaElement may have also had set/updatePixels, etc. called
          // on it and should be updated, or may have been set for the first
          // time!
          updated = true;
          data.setModified(false);
        } else if (data.loadedmetadata) {
          // if the meta data has been loaded, we can ask the video
          // what it's current position (in time) is.
          if (this._videoPrevUpdateTime !== data.time()) {
            // update the texture in gpu mem only if the current
            // video timestamp does not match the timestamp of the last
            // time we uploaded this texture (and update the time we
            // last uploaded, too)
            this._videoPrevUpdateTime = data.time();
            updated = true;
          }
        }
      } else if (this.isImageData) {
        if (data._dirty) {
          data._dirty = false;
          updated = true;
        }
      } else {
        /* data instanceof p5.Graphics, probably */
        // there is not enough information to tell if the texture can be
        // conditionally updated; so to be safe, we just go ahead and upload it.
        updated = true;
      }

      return updated;
    }

    bindTexture() {
      this._renderer.bindTexture(this);
      return this;
    }

    unbindTexture () {
      this._renderer.unbindTexture();
    }

    getTexture() {
      if (this.isFramebufferTexture) {
        return this.src.rawTexture();
      } else {
        return this.textureHandle;
      }
    }

    getSampler() {
      return this._renderer.getSampler(this);
    }

    setInterpolation(minFilter, magFilter) {
      this.minFilter = minFilter;
      this.magFilter = magFilter;
      this._renderer.setTextureParams(this);
    }

    setWrapMode(wrapX, wrapY) {
      this.wrapS = wrapX;
      this.wrapT = wrapY;
      this._renderer.setTextureParams(this);
    }
  }

  class MipmapTexture extends Texture {
    constructor(renderer, levels, settings = {}) {
      // Set default mipmap filtering
      const mipmapSettings = {
        minFilter: LINEAR,
        magFilter: LINEAR,
        ...settings
      };

      super(renderer, levels, mipmapSettings);
      this.levels = levels;
    }

    _getTextureDataFromSource() {
      return this.src;
    }

    init(levels) {
      // Handle both ImageData array (WebGL) and WebGPU texture object
      if (Array.isArray(levels)) {
        // WebGL path: levels is array of ImageData
        const firstLevel = levels[0];
        this.width = firstLevel.width;
        this.height = firstLevel.height;

        // Let renderer create the mipmap texture handle
        this.textureHandle = this._renderer.createMipmapTextureHandle({
          levels: levels,
          format: this.format,
          dataType: this.dataType,
          width: this.width,
          height: this.height,
        });
      } else {
        // WebGPU path: levels is a mipmapData object with pre-built GPU texture
        this.width = levels.size;
        this.height = levels.size;

        // Let renderer create the texture handle from the GPU texture
        this.textureHandle = this._renderer.createMipmapTextureHandle({
          gpuTexture: levels.gpuTexture,
          format: levels.format,
          dataType: 'uint8',
          width: this.width,
          height: this.height,
        });
      }

      this._renderer.setTextureParams(this, {
        minFilter: this.minFilter,
        magFilter: this.magFilter,
        wrapS: this.wrapS,
        wrapT: this.wrapT
      });
    }

    update() {}
  }

  function texture(p5, fn){
    /**
     * Texture class for WEBGL Mode
     * @private
     * @class p5.Texture
     * @param {p5.RendererGL} renderer an instance of p5.RendererGL that
     * will provide the GL context for this new p5.Texture
     * @param {p5.Image|p5.Graphics|p5.Element|p5.MediaElement|ImageData|p5.Framebuffer|p5.FramebufferTexture|ImageData} [obj] the
     * object containing the image data to store in the texture.
     * @param {Object} [settings] optional A javascript object containing texture
     * settings.
     * @param {Number} [settings.format] optional The internal color component
     * format for the texture. Possible values for format include gl.RGBA,
     * gl.RGB, gl.ALPHA, gl.LUMINANCE, gl.LUMINANCE_ALPHA. Defaults to gl.RBGA
     * @param {Number} [settings.minFilter] optional The texture minification
     * filter setting. Possible values are gl.NEAREST or gl.LINEAR. Defaults
     * to gl.LINEAR. Note, Mipmaps are not implemented in p5.
     * @param {Number} [settings.magFilter] optional The texture magnification
     * filter setting. Possible values are gl.NEAREST or gl.LINEAR. Defaults
     * to gl.LINEAR. Note, Mipmaps are not implemented in p5.
     * @param {Number} [settings.wrapS] optional The texture wrap settings for
     * the s coordinate, or x axis. Possible values are gl.CLAMP_TO_EDGE,
     * gl.REPEAT, and gl.MIRRORED_REPEAT. The mirror settings are only available
     * when using a power of two sized texture. Defaults to gl.CLAMP_TO_EDGE
     * @param {Number} [settings.wrapT] optional The texture wrap settings for
     * the t coordinate, or y axis. Possible values are gl.CLAMP_TO_EDGE,
     * gl.REPEAT, and gl.MIRRORED_REPEAT. The mirror settings are only available
     * when using a power of two sized texture. Defaults to gl.CLAMP_TO_EDGE
     * @param {Number} [settings.dataType] optional The data type of the texel
     * data. Possible values are gl.UNSIGNED_BYTE or gl.FLOAT. There are more
     * formats that are not implemented in p5. Defaults to gl.UNSIGNED_BYTE.
     */
    p5.Texture = Texture;

    p5.MipmapTexture = MipmapTexture;
  }

  if(typeof p5 !== 'undefined'){
    texture(p5);
  }

  /**
   * @private
   * @param {Uint8Array|Float32Array|undefined} pixels An existing pixels array to reuse if the size is the same
   * @param {WebGLRenderingContext} gl The WebGL context
   * @param {WebGLFramebuffer|null} framebuffer The Framebuffer to read
   * @param {Number} x The x coordiante to read, premultiplied by pixel density
   * @param {Number} y The y coordiante to read, premultiplied by pixel density
   * @param {Number} width The width in pixels to be read (factoring in pixel density)
   * @param {Number} height The height in pixels to be read (factoring in pixel density)
   * @param {GLEnum} format Either RGB or RGBA depending on how many channels to read
   * @param {GLEnum} type The datatype of each channel, e.g. UNSIGNED_BYTE or FLOAT
   * @param {Number|undefined} flipY If provided, the total height with which to flip the y axis about
   * @returns {Uint8Array|Float32Array} pixels A pixels array with the current state of the
   * WebGL context read into it
   */
  function readPixelsWebGL(
    pixels,
    gl,
    framebuffer,
    x,
    y,
    width,
    height,
    format,
    type,
    flipY,
  ) {
    // Record the currently bound framebuffer so we can go back to it after, and
    // bind the framebuffer we want to read from
    const prevFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);

    const channels = format === gl.RGBA ? 4 : 3;

    // Make a pixels buffer if it doesn't already exist
    const len = width * height * channels;
    const TypedArrayClass = type === gl.UNSIGNED_BYTE ? Uint8Array : Float32Array;
    if (!(pixels instanceof TypedArrayClass) || pixels.length !== len) {
      pixels = new TypedArrayClass(len);
    }

    gl.readPixels(
      x,
      flipY ? flipY - y - height : y,
      width,
      height,
      format,
      type,
      pixels,
    );

    // Re-bind whatever was previously bound
    gl.bindFramebuffer(gl.FRAMEBUFFER, prevFramebuffer);

    if (flipY) {
      // WebGL pixels are inverted compared to 2D pixels, so we have to flip
      // the resulting rows. Adapted from https://stackoverflow.com/a/41973289
      const halfHeight = Math.floor(height / 2);
      const tmpRow = new TypedArrayClass(width * channels);
      for (let y = 0; y < halfHeight; y++) {
        const topOffset = y * width * 4;
        const bottomOffset = (height - y - 1) * width * 4;
        tmpRow.set(pixels.subarray(topOffset, topOffset + width * 4));
        pixels.copyWithin(topOffset, bottomOffset, bottomOffset + width * 4);
        pixels.set(tmpRow, bottomOffset);
      }
    }

    return pixels;
  }

  /**
   * @private
   * @param {WebGLRenderingContext} gl The WebGL context
   * @param {WebGLFramebuffer|null} framebuffer The Framebuffer to read
   * @param {Number} x The x coordinate to read, premultiplied by pixel density
   * @param {Number} y The y coordinate to read, premultiplied by pixel density
   * @param {GLEnum} format Either RGB or RGBA depending on how many channels to read
   * @param {GLEnum} type The datatype of each channel, e.g. UNSIGNED_BYTE or FLOAT
   * @param {Number|undefined} flipY If provided, the total height with which to flip the y axis about
   * @returns {Number[]} pixels The channel data for the pixel at that location
   */
  function readPixelWebGL(gl, framebuffer, x, y, format, type, flipY) {
    // Record the currently bound framebuffer so we can go back to it after, and
    // bind the framebuffer we want to read from
    const prevFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);

    const channels = format === gl.RGBA ? 4 : 3;
    const TypedArrayClass = type === gl.UNSIGNED_BYTE ? Uint8Array : Float32Array;
    const pixels = new TypedArrayClass(channels);

    gl.readPixels(x, flipY ? flipY - y - 1 : y, 1, 1, format, type, pixels);

    // Re-bind whatever was previously bound
    gl.bindFramebuffer(gl.FRAMEBUFFER, prevFramebuffer);

    return Array.from(pixels);
  }

  function setWebGLTextureParams(texture, gl, webglVersion) {
    texture.bindTexture();
    const glMinFilter =
      texture.minFilter === NEAREST ? gl.NEAREST :
        texture.minFilter === LINEAR_MIPMAP ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR;
    const glMagFilter =
      texture.magFilter === NEAREST ? gl.NEAREST : gl.LINEAR;

    // for webgl 1 we need to check if the texture is power of two
    // if it isn't we will set the wrap mode to CLAMP
    // webgl2 will support npot REPEAT and MIRROR but we don't check for it yet
    const isPowerOfTwo = (x) => (x & (x - 1)) === 0;
    const textureData = texture._getTextureDataFromSource();

    let wrapWidth;
    let wrapHeight;

    if (textureData.naturalWidth && textureData.naturalHeight) {
      wrapWidth = textureData.naturalWidth;
      wrapHeight = textureData.naturalHeight;
    } else {
      wrapWidth = texture.width;
      wrapHeight = texture.height;
    }

    const widthPowerOfTwo = isPowerOfTwo(wrapWidth);
    const heightPowerOfTwo = isPowerOfTwo(wrapHeight);
    let glWrapS, glWrapT;

    if (texture.wrapS === REPEAT) {
      if (
        webglVersion === WEBGL2 ||
        (widthPowerOfTwo && heightPowerOfTwo)
      ) {
        glWrapS = gl.REPEAT;
      } else {
        console.warn(
          "You tried to set the wrap mode to REPEAT but the texture size is not a power of two. Setting to CLAMP instead",
        );
        glWrapS = gl.CLAMP_TO_EDGE;
      }
    } else if (texture.wrapS === MIRROR) {
      if (
        webglVersion === WEBGL2 ||
        (widthPowerOfTwo && heightPowerOfTwo)
      ) {
        glWrapS = gl.MIRRORED_REPEAT;
      } else {
        console.warn(
          "You tried to set the wrap mode to MIRROR but the texture size is not a power of two. Setting to CLAMP instead",
        );
        glWrapS = gl.CLAMP_TO_EDGE;
      }
    } else {
      // falling back to default if didn't get a proper mode
      glWrapS = gl.CLAMP_TO_EDGE;
    }

    if (texture.wrapT === REPEAT) {
      if (
        webglVersion === WEBGL2 ||
        (widthPowerOfTwo && heightPowerOfTwo)
      ) {
        glWrapT = gl.REPEAT;
      } else {
        console.warn(
          "You tried to set the wrap mode to REPEAT but the texture size is not a power of two. Setting to CLAMP instead",
        );
        glWrapT = gl.CLAMP_TO_EDGE;
      }
    } else if (texture.wrapT === MIRROR) {
      if (
        webglVersion === WEBGL2 ||
        (widthPowerOfTwo && heightPowerOfTwo)
      ) {
        glWrapT = gl.MIRRORED_REPEAT;
      } else {
        console.warn(
          "You tried to set the wrap mode to MIRROR but the texture size is not a power of two. Setting to CLAMP instead",
        );
        glWrapT = gl.CLAMP_TO_EDGE;
      }
    } else {
      // falling back to default if didn't get a proper mode
      glWrapT = gl.CLAMP_TO_EDGE;
    }

    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, glMinFilter);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, glMagFilter);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, glWrapS);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, glWrapT);
    texture.unbindTexture();
  }

  function setWebGLUniformValue(shader, uniform, data, getTexture, gl) {
    const location = uniform.location;
    shader.useProgram();

    switch (uniform.type) {
      case gl.BOOL:
        if (data === true) {
          gl.uniform1i(location, 1);
        } else {
          gl.uniform1i(location, 0);
        }
        break;
      case gl.INT:
        if (uniform.size > 1) {
          data.length && gl.uniform1iv(location, data);
        } else {
          gl.uniform1i(location, data);
        }
        break;
      case gl.FLOAT:
        if (uniform.size > 1) {
          data.length && gl.uniform1fv(location, data);
        } else {
          gl.uniform1f(location, data);
        }
        break;
      case gl.FLOAT_MAT3:
        gl.uniformMatrix3fv(location, false, data);
        break;
      case gl.FLOAT_MAT4:
        gl.uniformMatrix4fv(location, false, data);
        break;
      case gl.FLOAT_VEC2:
        if (uniform.size > 1) {
          data.length && gl.uniform2fv(location, data);
        } else {
          gl.uniform2f(location, data[0], data[1]);
        }
        break;
      case gl.FLOAT_VEC3:
        if (uniform.size > 1) {
          data.length && gl.uniform3fv(location, data);
        } else {
          gl.uniform3f(location, data[0], data[1], data[2]);
        }
        break;
      case gl.FLOAT_VEC4:
        if (uniform.size > 1) {
          data.length && gl.uniform4fv(location, data);
        } else {
          gl.uniform4f(location, data[0], data[1], data[2], data[3]);
        }
        break;
      case gl.INT_VEC2:
        if (uniform.size > 1) {
          data.length && gl.uniform2iv(location, data);
        } else {
          gl.uniform2i(location, data[0], data[1]);
        }
        break;
      case gl.INT_VEC3:
        if (uniform.size > 1) {
          data.length && gl.uniform3iv(location, data);
        } else {
          gl.uniform3i(location, data[0], data[1], data[2]);
        }
        break;
      case gl.INT_VEC4:
        if (uniform.size > 1) {
          data.length && gl.uniform4iv(location, data);
        } else {
          gl.uniform4i(location, data[0], data[1], data[2], data[3]);
        }
        break;
      case gl.SAMPLER_2D:
        if (typeof data == "number") {
          if (
            data < gl.TEXTURE0 ||
            data > gl.TEXTURE31 ||
            data !== Math.ceil(data)
          ) {
            console.log(
              "🌸 p5.js says: " +
                "You're trying to use a number as the data for a texture." +
                "Please use a texture.",
            );
            return;
          }
          gl.activeTexture(data);
          gl.uniform1i(location, data);
        } else {
          gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex);
          uniform.texture = data instanceof Texture ? data : getTexture(data);
          gl.uniform1i(location, uniform.samplerIndex);
        }
        break;
      case gl.SAMPLER_CUBE:
      case gl.SAMPLER_3D:
      case gl.SAMPLER_2D_SHADOW:
      case gl.SAMPLER_2D_ARRAY:
      case gl.SAMPLER_2D_ARRAY_SHADOW:
      case gl.SAMPLER_CUBE_SHADOW:
      case gl.INT_SAMPLER_2D:
      case gl.INT_SAMPLER_3D:
      case gl.INT_SAMPLER_CUBE:
      case gl.INT_SAMPLER_2D_ARRAY:
      case gl.UNSIGNED_INT_SAMPLER_2D:
      case gl.UNSIGNED_INT_SAMPLER_3D:
      case gl.UNSIGNED_INT_SAMPLER_CUBE:
      case gl.UNSIGNED_INT_SAMPLER_2D_ARRAY:
        if (typeof data !== "number") {
          break;
        }
        if (
          data < gl.TEXTURE0 ||
          data > gl.TEXTURE31 ||
          data !== Math.ceil(data)
        ) {
          console.log(
            "🌸 p5.js says: " +
              "You're trying to use a number as the data for a texture." +
              "Please use a texture.",
          );
          break;
        }
        gl.activeTexture(data);
        gl.uniform1i(location, data);
        break;
      //@todo complete all types
    }
  }

  function getWebGLUniformMetadata(shader, gl) {
    const program = shader._glProgram;

    const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
    const result = [];

    let samplerIndex = 0;

    for (let i = 0; i < numUniforms; ++i) {
      const uniformInfo = gl.getActiveUniform(program, i);
      const uniform = {};
      uniform.location = gl.getUniformLocation(program, uniformInfo.name);
      uniform.size = uniformInfo.size;
      let uniformName = uniformInfo.name;
      //uniforms that are arrays have their name returned as
      //someUniform[0] which is a bit silly so we trim it
      //off here. The size property tells us that its an array
      //so we dont lose any information by doing this
      if (uniformInfo.size > 1) {
        uniformName = uniformName.substring(0, uniformName.indexOf("[0]"));
      }
      uniform.name = uniformName;
      uniform.type = uniformInfo.type;
      uniform._cachedData = undefined;
      if (uniform.type === gl.SAMPLER_2D) {
        uniform.isSampler = true;
        uniform.samplerIndex = samplerIndex;
        samplerIndex++;
      }

      uniform.isArray =
        uniformInfo.size > 1 ||
        uniform.type === gl.FLOAT_MAT3 ||
        uniform.type === gl.FLOAT_MAT4 ||
        uniform.type === gl.FLOAT_VEC2 ||
        uniform.type === gl.FLOAT_VEC3 ||
        uniform.type === gl.FLOAT_VEC4 ||
        uniform.type === gl.INT_VEC2 ||
        uniform.type === gl.INT_VEC4 ||
        uniform.type === gl.INT_VEC3;

      result.push(uniform);
    }

    return result;
  }

  function getWebGLShaderAttributes(shader, gl) {
    const attributes = {};

    const numAttributes = gl.getProgramParameter(
      shader._glProgram,
      gl.ACTIVE_ATTRIBUTES,
    );
    for (let i = 0; i < numAttributes; ++i) {
      const attributeInfo = gl.getActiveAttrib(shader._glProgram, i);
      const name = attributeInfo.name;
      const location = gl.getAttribLocation(shader._glProgram, name);
      const attribute = {};
      attribute.name = name;
      attribute.location = location;
      attribute.index = i;
      attribute.type = attributeInfo.type;
      attribute.size = attributeInfo.size;
      attributes[name] = attribute;
    }

    return attributes;
  }

  function populateGLSLHooks(shader, src, shaderType) {
    const main = "void main";
    if (!src.includes(main)) return src;

    let [preMain, postMain] = src.split(main);

    let hooks = "";
    let defines = "";
    for (const key in shader.hooks.uniforms) {
      hooks += `uniform ${key};\n`;
    }
    if (shader.hooks.declarations) {
      hooks += shader.hooks.declarations + "\n";
    }
    if (shader.hooks[shaderType].declarations) {
      hooks += shader.hooks[shaderType].declarations + "\n";
    }

    // Handle varying variables from p5.strands
    if (
      shader.hooks.varyingVariables &&
      shader.hooks.varyingVariables.length > 0
    ) {
      for (const varyingVar of shader.hooks.varyingVariables) {
        // Generate OUT declaration for vertex shader, IN declaration for fragment shader
        if (shaderType === "vertex") {
          hooks += `OUT ${varyingVar};\n`;
        } else if (shaderType === "fragment") {
          hooks += `IN ${varyingVar};\n`;
        }
      }
    }
    for (const hookDef in shader.hooks.helpers) {
      hooks += `${hookDef}${shader.hooks.helpers[hookDef]}\n`;
    }
    for (const hookDef in shader.hooks[shaderType]) {
      if (hookDef === "declarations") continue;
      const [hookType, hookName] = hookDef.split(" ");

      // Add a #define so that if the shader wants to use preprocessor directives to
      // optimize away the extra function calls in main, it can do so
      if (
        shader.hooks.modified.vertex[hookDef] ||
        shader.hooks.modified.fragment[hookDef]
      ) {
        defines += "#define AUGMENTED_HOOK_" + hookName + "\n";
      }

      hooks +=
        hookType + " HOOK_" + hookName + shader.hooks[shaderType][hookDef] + "\n";
    }

    // Allow shaders to specify the location of hook #define statements. Normally these
    // go after function definitions, but one might want to have them defined earlier
    // in order to only conditionally make uniforms.
    if (preMain.indexOf("#define HOOK_DEFINES") !== -1) {
      preMain = preMain.replace("#define HOOK_DEFINES", "\n" + defines + "\n");
      defines = "";
    }

    return preMain + "\n" + defines + hooks + main + postMain;
  }

  function checkWebGLCapabilities({ GL, webglVersion }) {
    const gl = GL;
    const supportsFloat =
      webglVersion === WEBGL2
        ? gl.getExtension("EXT_color_buffer_float") &&
          gl.getExtension("EXT_float_blend")
        : gl.getExtension("OES_texture_float");
    const supportsFloatLinear =
      supportsFloat && gl.getExtension("OES_texture_float_linear");
    const supportsHalfFloat =
      webglVersion === WEBGL2
        ? gl.getExtension("EXT_color_buffer_float")
        : gl.getExtension("OES_texture_half_float");
    const supportsHalfFloatLinear =
      supportsHalfFloat && gl.getExtension("OES_texture_half_float_linear");
    return {
      float: supportsFloat,
      floatLinear: supportsFloatLinear,
      halfFloat: supportsHalfFloat,
      halfFloatLinear: supportsHalfFloatLinear,
    };
  }

  /**
   * @module Rendering
   * @requires constants
   */


  const constrain = (n, low, high) => Math.max(Math.min(n, high), low);

  class FramebufferCamera extends Camera {
    constructor(framebuffer) {
      super(framebuffer.renderer);
      this.fbo = framebuffer;

      this.yScale = framebuffer.renderer.framebufferYScale();
    }

    _computeCameraDefaultSettings() {
      super._computeCameraDefaultSettings();
      this.defaultAspectRatio = this.fbo.width / this.fbo.height;
      this.defaultCameraFOV =
        2 * Math.atan(this.fbo.height / 2 / this.defaultEyeZ);
    }

    copy() {
      const _cam = super.copy();
      _cam.fbo = this.fbo;
      return _cam;
    }
  }

  class FramebufferTexture {
    constructor(framebuffer, property) {
      this.framebuffer = framebuffer;
      this.property = property;
    }

    get width() {
      return this.framebuffer.width * this.framebuffer.density;
    }

    get height() {
      return this.framebuffer.height * this.framebuffer.density;
    }

    update() {
      this.framebuffer._update(this.property);
    }

    rawTexture() {
      return { texture: this.framebuffer[this.property] };
    }
  }

  let Framebuffer$1 = class Framebuffer {
    constructor(renderer, settings = {}) {
      this.renderer = renderer;
      this.renderer.framebuffers.add(this);

      this._isClipApplied = false;
      this._useCanvasFormat = settings._useCanvasFormat || false;

      this.dirty = { colorTexture: false, depthTexture: false };

      this.pixels = [];

      this.format = settings.format || UNSIGNED_BYTE;
      this.channels = settings.channels || (
        this.renderer.defaultFramebufferAlpha()
          ? RGBA
          : RGB
      );
      this.useDepth = settings.depth === undefined ? true : settings.depth;
      this.depthFormat = settings.depthFormat || FLOAT;
      this.textureFiltering = settings.textureFiltering || LINEAR;
      if (settings.antialias === undefined) {
        this.antialiasSamples = this.renderer.defaultFramebufferAntialias()
          ? 2
          : 0;
      } else if (typeof settings.antialias === 'number') {
        this.antialiasSamples = settings.antialias;
      } else {
        this.antialiasSamples = settings.antialias ? 2 : 0;
      }
      this.antialias = this.antialiasSamples > 0;
      if (this.antialias && !this.renderer.supportsFramebufferAntialias()) {
        console.warn('Framebuffer antialiasing is unsupported in this context');
        this.antialias = false;
      }
      this.density = settings.density || this.renderer._pixelDensity;
      if (settings.width && settings.height) {
        const dimensions =
          this.renderer._adjustDimensions(settings.width, settings.height);
        this.width = dimensions.adjustedWidth;
        this.height = dimensions.adjustedHeight;
        this._autoSized = false;
      } else {
        if ((settings.width === undefined) !== (settings.height === undefined)) {
          console.warn(
            'Please supply both width and height for a framebuffer to give it a ' +
              'size. Only one was given, so the framebuffer will match the size ' +
              'of its canvas.'
          );
        }
        this.width = this.renderer.width;
        this.height = this.renderer.height;
        this._autoSized = true;
      }
      // Let renderer validate and adjust formats for this context
      this.renderer.validateFramebufferFormats(this);

      if (settings.stencil && !this.useDepth) {
        console.warn('A stencil buffer can only be used if also using depth. Since the framebuffer has no depth buffer, the stencil buffer will be ignored.');
      }
      this.useStencil = this.useDepth &&
        (settings.stencil === undefined ? true : settings.stencil);

      // Let renderer create framebuffer resources with antialiasing support
      this.renderer.createFramebufferResources(this);

      this._recreateTextures();

      this.defaultCamera = this.createCamera();
      this.filterCamera = this.createCamera();

      this.draw(() => this.renderer.clear());
    }

    /**
     * Resizes the framebuffer to a given width and height.
     *
     * The parameters, `width` and `height`, set the dimensions of the
     * framebuffer. For example, calling `myBuffer.resize(300, 500)` resizes
     * the framebuffer to 300×500 pixels, then sets `myBuffer.width` to 300
     * and `myBuffer.height` 500.
     *
     * @param {Number} width width of the framebuffer.
     * @param {Number} height height of the framebuffer.
     *
     * @example
     * let myBuffer;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Framebuffer object.
     *   myBuffer = createFramebuffer();
     *
     *   describe('A multicolor sphere on a white surface. The image grows larger or smaller when the user moves the mouse, revealing a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw to the p5.Framebuffer object.
     *   myBuffer.begin();
     *   background(255);
     *   normalMaterial();
     *   sphere(20);
     *   myBuffer.end();
     *
     *   // Display the p5.Framebuffer object.
     *   image(myBuffer, -50, -50);
     * }
     *
     * // Resize the p5.Framebuffer object when the
     * // user moves the mouse.
     * function mouseMoved() {
     *   myBuffer.resize(mouseX, mouseY);
     * }
     */
    resize(width, height) {
      this._autoSized = false;
      const dimensions =
        this.renderer._adjustDimensions(width, height);
      width = dimensions.adjustedWidth;
      height = dimensions.adjustedHeight;
      this.width = width;
      this.height = height;
      this._handleResize();
    }

    /**
     * Sets the framebuffer's pixel density or returns its current density.
     *
     * Computer displays are grids of little lights called pixels. A display's
     * pixel density describes how many pixels it packs into an area. Displays
     * with smaller pixels have a higher pixel density and create sharper
     * images.
     *
     * The parameter, `density`, is optional. If a number is passed, as in
     * `myBuffer.pixelDensity(1)`, it sets the framebuffer's pixel density. By
     * default, the framebuffer's pixel density will match that of the canvas
     * where it was created. All canvases default to match the display's pixel
     * density.
     *
     * Calling `myBuffer.pixelDensity()` without an argument returns its current
     * pixel density.
     *
     * @param {Number} [density] pixel density to set.
     * @returns {Number} current pixel density.
     *
     * @example
     * let myBuffer;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Framebuffer object.
     *   myBuffer = createFramebuffer();
     *
     *   describe("A white circle on a gray canvas. The circle's edge become fuzzy while the user presses and holds the mouse.");
     * }
     *
     * function draw() {
     *   // Draw to the p5.Framebuffer object.
     *   myBuffer.begin();
     *   background(200);
     *   circle(0, 0, 40);
     *   myBuffer.end();
     *
     *   // Display the p5.Framebuffer object.
     *   image(myBuffer, -50, -50);
     * }
     *
     * // Decrease the pixel density when the user
     * // presses the mouse.
     * function mousePressed() {
     *   myBuffer.pixelDensity(1);
     * }
     *
     * // Increase the pixel density when the user
     * // releases the mouse.
     * function mouseReleased() {
     *   myBuffer.pixelDensity(2);
     * }
     *
     * @example
     * let myBuffer;
     * let myFont;
     *
     * async function setup() {
     *   // Load a font and create a p5.Font object.
     *   myFont = await loadFont('assets/inconsolata.otf');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   // Create a p5.Framebuffer object.
     *   myBuffer = createFramebuffer();
     *
     *   // Get the p5.Framebuffer object's pixel density.
     *   let d = myBuffer.pixelDensity();
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont(myFont);
     *   textSize(16);
     *   fill(0);
     *
     *   // Display the pixel density.
     *   text(`Density: ${d}`, 0, 0);
     *
     *   describe(`The text "Density: ${d}" written in black on a gray background.`);
     * }
     */
    pixelDensity(density) {
      if (density) {
        this._autoSized = false;
        this.density = density;
        this._handleResize();
      } else {
        return this.density;
      }
    }

    /**
     * Toggles the framebuffer's autosizing mode or returns the current mode.
     *
     * By default, the framebuffer automatically resizes to match the canvas
     * that created it. Calling `myBuffer.autoSized(false)` disables this
     * behavior and calling `myBuffer.autoSized(true)` re-enables it.
     *
     * Calling `myBuffer.autoSized()` without an argument returns `true` if
     * the framebuffer automatically resizes and `false` if not.
     *
     * @param {Boolean} [autoSized] whether to automatically resize the framebuffer to match the canvas.
     * @returns {Boolean} current autosize setting.
     *
     * @example
     * // Double-click to toggle the autosizing mode.
     *
     * let myBuffer;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Framebuffer object.
     *   myBuffer = createFramebuffer();
     *
     *   describe('A multicolor sphere on a gray background. The image resizes when the user moves the mouse.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Draw to the p5.Framebuffer object.
     *   myBuffer.begin();
     *   background(200);
     *   normalMaterial();
     *   sphere(width / 4);
     *   myBuffer.end();
     *
     *   // Display the p5.Framebuffer object.
     *   image(myBuffer, -width / 2, -height / 2);
     * }
     *
     * // Resize the canvas when the user moves the mouse.
     * function mouseMoved() {
     *   let w = constrain(mouseX, 0, 100);
     *   let h = constrain(mouseY, 0, 100);
     *   resizeCanvas(w, h);
     * }
     *
     * // Toggle autoSizing when the user double-clicks.
     * // Note: opened an issue to fix(?) this.
     * function doubleClicked() {
     *   let isAuto = myBuffer.autoSized();
     *   myBuffer.autoSized(!isAuto);
     * }
     */
    autoSized(autoSized) {
      if (autoSized === undefined) {
        return this._autoSized;
      } else {
        this._autoSized = autoSized;
        this._handleResize();
      }
    }

    /**
     * Checks the capabilities of the current WebGL environment to see if the
     * settings supplied by the user are capable of being fulfilled. If they
     * are not, warnings will be logged and the settings will be changed to
     * something close that can be fulfilled.
     *
     * @private
     */
    _checkIfFormatsAvailable() {
      const gl = this.gl;

      if (
        this.useDepth &&
        this.renderer.webglVersion === WEBGL &&
        !gl.getExtension('WEBGL_depth_texture')
      ) {
        console.warn(
          'Unable to create depth textures in this environment. Falling back ' +
            'to a framebuffer without depth.'
        );
        this.useDepth = false;
      }

      if (
        this.useDepth &&
        this.renderer.webglVersion === WEBGL &&
        this.depthFormat === FLOAT
      ) {
        console.warn(
          'FLOAT depth format is unavailable in WebGL 1. ' +
            'Defaulting to UNSIGNED_INT.'
        );
        this.depthFormat = UNSIGNED_INT;
      }

      if (![
        UNSIGNED_BYTE,
        FLOAT,
        HALF_FLOAT
      ].includes(this.format)) {
        console.warn(
          'Unknown Framebuffer format. ' +
            'Please use UNSIGNED_BYTE, FLOAT, or HALF_FLOAT. ' +
            'Defaulting to UNSIGNED_BYTE.'
        );
        this.format = UNSIGNED_BYTE;
      }
      if (this.useDepth && ![
        UNSIGNED_INT,
        FLOAT
      ].includes(this.depthFormat)) {
        console.warn(
          'Unknown Framebuffer depth format. ' +
            'Please use UNSIGNED_INT or FLOAT. Defaulting to FLOAT.'
        );
        this.depthFormat = FLOAT;
      }

      const support = checkWebGLCapabilities(this.renderer);
      if (!support.float && this.format === FLOAT) {
        console.warn(
          'This environment does not support FLOAT textures. ' +
            'Falling back to UNSIGNED_BYTE.'
        );
        this.format = UNSIGNED_BYTE;
      }
      if (
        this.useDepth &&
        !support.float &&
        this.depthFormat === FLOAT
      ) {
        console.warn(
          'This environment does not support FLOAT depth textures. ' +
            'Falling back to UNSIGNED_INT.'
        );
        this.depthFormat = UNSIGNED_INT;
      }
      if (!support.halfFloat && this.format === HALF_FLOAT) {
        console.warn(
          'This environment does not support HALF_FLOAT textures. ' +
            'Falling back to UNSIGNED_BYTE.'
        );
        this.format = UNSIGNED_BYTE;
      }

      if (
        this.channels === RGB &&
        [FLOAT, HALF_FLOAT].includes(this.format)
      ) {
        console.warn(
          'FLOAT and HALF_FLOAT formats do not work cross-platform with only ' +
            'RGB channels. Falling back to RGBA.'
        );
        this.channels = RGBA;
      }
    }

    _deleteTextures() {
      this.renderer.deleteFramebufferTextures(this);
    }

    /**
     * Creates new textures and renderbuffers given the current size of the
     * framebuffer.
     *
     * @private
     */
    _recreateTextures() {
      this._updateSize();

      // Let renderer handle texture creation and framebuffer setup
      this.renderer.recreateFramebufferTextures(this);

      if (this.useDepth) {
        this.depth = new FramebufferTexture(this, 'depthTexture');
        const depthFilter = NEAREST;
        this.depthP5Texture = new Texture(
          this.renderer,
          this.depth,
          {
            minFilter: depthFilter,
            magFilter: depthFilter
          }
        );
        this.renderer.textures.set(this.depth, this.depthP5Texture);
      }

      this.color = new FramebufferTexture(this, 'colorTexture');
      const filter = this.textureFiltering === LINEAR
        ? LINEAR
        : NEAREST;
      this.colorP5Texture = new Texture(
        this.renderer,
        this.color,
        {
          minFilter: filter,
          magFilter: filter
        }
      );
      this.renderer.textures.set(this.color, this.colorP5Texture);
    }

    /**
     * A method that will be called when recreating textures. If the framebuffer
     * is auto-sized, it will update its width, height, and density properties.
     *
     * @private
     */
    _updateSize() {
      if (this._autoSized) {
        this.width = this.renderer.width;
        this.height = this.renderer.height;
        this.density = this.renderer._pixelDensity;
      }
    }

    /**
     * Called when the canvas that the framebuffer is attached to resizes. If the
     * framebuffer is auto-sized, it will update its textures to match the new
     * size.
     *
     * @private
     */
    _canvasSizeChanged() {
      if (this._autoSized) {
        this._handleResize();
      }
    }

    /**
     * Called when the size of the framebuffer has changed (either by being
     * manually updated or from auto-size updates when its canvas changes size.)
     * Old textures and renderbuffers will be deleted, and then recreated with the
     * new size.
     *
     * @private
     */
    _handleResize() {
      this._deleteTextures();
      this._recreateTextures();
      this.defaultCamera._resize();
    }

    /**
     * Creates a new
     * <a href="#/p5.Camera">p5.Camera</a> object to use with the framebuffer.
     *
     * The new camera is initialized with a default position `(0, 0, 800)` and a
     * default perspective projection. Its properties can be controlled with
     * <a href="#/p5.Camera">p5.Camera</a> methods such as `myCamera.lookAt(0, 0, 0)`.
     *
     * Framebuffer cameras should be created between calls to
     * <a href="#/p5.Framebuffer/begin">myBuffer.begin()</a> and
     * <a href="#/p5.Framebuffer/end">myBuffer.end()</a> like so:
     *
     * ```js
     * let myCamera;
     *
     * myBuffer.begin();
     *
     * // Create the camera for the framebuffer.
     * myCamera = myBuffer.createCamera();
     *
     * myBuffer.end();
     * ```
     *
     * Calling <a href="#/p5/setCamera">setCamera()</a> updates the
     * framebuffer's projection using the camera.
     * <a href="#/p5/resetMatrix">resetMatrix()</a> must also be called for the
     * view to change properly:
     *
     * ```js
     * myBuffer.begin();
     *
     * // Set the camera for the framebuffer.
     * setCamera(myCamera);
     *
     * // Reset all transformations.
     * resetMatrix();
     *
     * // Draw stuff...
     *
     * myBuffer.end();
     * ```
     *
     * @returns {p5.Camera} new camera.
     *
     * @example
     * // Double-click to toggle between cameras.
     *
     * let myBuffer;
     * let cam1;
     * let cam2;
     * let usingCam1 = true;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Framebuffer object.
     *   myBuffer = createFramebuffer();
     *
     *   // Create the cameras between begin() and end().
     *   myBuffer.begin();
     *
     *   // Create the first camera.
     *   // Keep its default settings.
     *   cam1 = myBuffer.createCamera();
     *
     *   // Create the second camera.
     *   // Place it at the top-left.
     *   // Point it at the origin.
     *   cam2 = myBuffer.createCamera();
     *   cam2.setPosition(400, -400, 800);
     *   cam2.lookAt(0, 0, 0);
     *
     *   myBuffer.end();
     *
     *   describe(
     *     'A white cube on a gray background. The camera toggles between frontal and aerial views when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   // Draw to the p5.Framebuffer object.
     *   myBuffer.begin();
     *   background(200);
     *
     *   // Set the camera.
     *   if (usingCam1 === true) {
     *     setCamera(cam1);
     *   } else {
     *     setCamera(cam2);
     *   }
     *
     *   // Reset all transformations.
     *   resetMatrix();
     *
     *   // Draw the box.
     *   box();
     *
     *   myBuffer.end();
     *
     *   // Display the p5.Framebuffer object.
     *   image(myBuffer, -50, -50);
     * }
     *
     * // Toggle the current camera when the user double-clicks.
     * function doubleClicked() {
     *   if (usingCam1 === true) {
     *     usingCam1 = false;
     *   } else {
     *     usingCam1 = true;
     *   }
     * }
     */
    createCamera() {
      const cam = new FramebufferCamera(this);
      cam._computeCameraDefaultSettings();
      cam._setDefaultCamera();
      return cam;
    }

    /**
     * Deletes the framebuffer from GPU memory.
     *
     * Calling `myBuffer.remove()` frees the GPU memory used by the framebuffer.
     * The framebuffer also uses a bit of memory on the CPU which can be freed
     * like so:
     *
     * ```js
     * // Delete the framebuffer from GPU memory.
     * myBuffer.remove();
     *
     * // Delete the framebuffer from CPU memory.
     * myBuffer = undefined;
     * ```
     *
     * Note: All variables that reference the framebuffer must be assigned
     * the value `undefined` to delete the framebuffer from CPU memory. If any
     * variable still refers to the framebuffer, then it won't be garbage
     * collected.
     *
     * @example
     * // Double-click to remove the p5.Framebuffer object.
     *
     * let myBuffer;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create an options object.
     *   let options = { width: 60, height: 60 };
     *
     *   // Create a p5.Framebuffer object and
     *   // configure it using options.
     *   myBuffer = createFramebuffer(options);
     *
     *   describe('A white circle at the center of a dark gray square disappears when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Display the p5.Framebuffer object if
     *   // it's available.
     *   if (myBuffer) {
     *     // Draw to the p5.Framebuffer object.
     *     myBuffer.begin();
     *     background(100);
     *     circle(0, 0, 20);
     *     myBuffer.end();
     *
     *     image(myBuffer, -30, -30);
     *   }
     * }
     *
     * // Remove the p5.Framebuffer object when the
     * // the user double-clicks.
     * function doubleClicked() {
     *   // Delete the framebuffer from GPU memory.
     *   myBuffer.remove();
     *
     *   // Delete the framebuffer from CPU memory.
     *   myBuffer = undefined;
     * }
     */
    remove() {
      this._deleteTextures();

      // Let renderer clean up framebuffer resources
      this.renderer.deleteFramebufferResources(this);

      this.renderer.framebuffers.delete(this);
    }

    /**
     * Begins drawing shapes to the framebuffer.
     *
     * `myBuffer.begin()` and <a href="#/p5.Framebuffer/end">myBuffer.end()</a>
     * allow shapes to be drawn to the framebuffer. `myBuffer.begin()` begins
     * drawing to the framebuffer and
     * <a href="#/p5.Framebuffer/end">myBuffer.end()</a> stops drawing to the
     * framebuffer. Changes won't be visible until the framebuffer is displayed
     * as an image or texture.
     *
     * @example
     * let myBuffer;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Framebuffer object.
     *   myBuffer = createFramebuffer();
     *
     *   describe('An empty gray canvas. The canvas gets darker and a rotating, multicolor torus appears while the user presses and holds the mouse.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Start drawing to the p5.Framebuffer object.
     *   myBuffer.begin();
     *
     *   background(50);
     *   rotateY(frameCount * 0.01);
     *   normalMaterial();
     *   torus(30);
     *
     *   // Stop drawing to the p5.Framebuffer object.
     *   myBuffer.end();
     *
     *   // Display the p5.Framebuffer object while
     *   // the user presses the mouse.
     *   if (mouseIsPressed === true) {
     *     image(myBuffer, -50, -50);
     *   }
     * }
     */
    begin() {
      this.prevFramebuffer = this.renderer.activeFramebuffer();
      if (this.prevFramebuffer) {
        this.prevFramebuffer._beforeEnd();
      }
      this.renderer.activeFramebuffers.push(this);
      this._beforeBegin();
      this.renderer.push();
      // Apply the framebuffer's camera. This does almost what
      // RendererGL.reset() does, but this does not try to clear any buffers;
      // it only sets the camera.
      // this.renderer.setCamera(this.defaultCamera);
      this.renderer.states.setValue('curCamera', this.defaultCamera);
      // set the projection matrix (which is not normally updated each frame)
      this.renderer.states.setValue('uPMatrix', this.renderer.states.uPMatrix.clone());
      this.renderer.states.uPMatrix.set(this.defaultCamera.projMatrix);
      this.renderer.states.setValue('uViewMatrix', this.renderer.states.uViewMatrix.clone());
      this.renderer.states.uViewMatrix.set(this.defaultCamera.cameraMatrix);

      this.renderer.resetMatrix();
      this.renderer.states.uViewMatrix
        .set(this.renderer.states.curCamera.cameraMatrix);
      this.renderer.states.uModelMatrix.reset();
      this.renderer._applyStencilTestIfClipping();
    }

    /**
     * When making a p5.Framebuffer active so that it may be drawn to, this method
     * returns the underlying WebGL framebuffer that needs to be active to
     * support this. Antialiased framebuffers first write to a multisampled
     * renderbuffer, while other framebuffers can write directly to their main
     * framebuffers.
     *
     * @private
     */
    _framebufferToBind() {
      return this.renderer.getFramebufferToBind(this);
    }

    /**
     * Ensure all readable textures are up-to-date.
     * @private
     * @param {'colorTexutre'|'depthTexture'} property The property to update
     */
    _update(property) {
      if (this.dirty[property]) {
        this.renderer.updateFramebufferTexture(this, property);
        this.dirty[property] = false;
      }
    }

    /**
     * Ensures that the framebuffer is ready to be drawn to
     *
     * @private
     */
    _beforeBegin() {
      this.renderer.bindFramebuffer(this);
      this.renderer.viewport(
        this.width * this.density,
        this.height * this.density
      );
      if (this.renderer.flushDraw) {
        this.renderer.flushDraw();
      }
    }

    /**
     * Ensures that the framebuffer is ready to be read by other framebuffers.
     *
     * @private
     */
    _beforeEnd() {
      if (this.antialias) {
        this.dirty = { colorTexture: true, depthTexture: true };
      }
      // TODO
      // This should work but flushes more often than we need to. Ideally we only do this
      // right before the fbo is read as a texture.
      if (this.renderer.flushDraw) {
        this.renderer.flushDraw();
      }
    }

    /**
     * Stops drawing shapes to the framebuffer.
     *
     * <a href="#/p5.Framebuffer/begin">myBuffer.begin()</a> and `myBuffer.end()`
     * allow shapes to be drawn to the framebuffer.
     * <a href="#/p5.Framebuffer/begin">myBuffer.begin()</a> begins drawing to
     * the framebuffer and `myBuffer.end()` stops drawing to the framebuffer.
     * Changes won't be visible until the framebuffer is displayed as an image
     * or texture.
     *
     * @example
     * let myBuffer;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Framebuffer object.
     *   myBuffer = createFramebuffer();
     *
     *   describe('An empty gray canvas. The canvas gets darker and a rotating, multicolor torus appears while the user presses and holds the mouse.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Start drawing to the p5.Framebuffer object.
     *   myBuffer.begin();
     *
     *   background(50);
     *   rotateY(frameCount * 0.01);
     *   normalMaterial();
     *   torus(30);
     *
     *   // Stop drawing to the p5.Framebuffer object.
     *   myBuffer.end();
     *
     *   // Display the p5.Framebuffer object while
     *   // the user presses the mouse.
     *   if (mouseIsPressed === true) {
     *     image(myBuffer, -50, -50);
     *   }
     * }
     */
    end() {
      this.renderer.pop();

      const fbo = this.renderer.activeFramebuffers.pop();
      if (fbo !== this) {
        throw new Error("It looks like you've called end() while another Framebuffer is active.");
      }
      this._beforeEnd();
      if (this.prevFramebuffer) {
        this.prevFramebuffer._beforeBegin();
      } else {
        this.renderer.bindFramebuffer(null);
        this.renderer.viewport(
          this.renderer._origViewport.width,
          this.renderer._origViewport.height
        );
      }
      this.renderer._applyStencilTestIfClipping();
    }

    /**
     * Draws to the framebuffer by calling a function that contains drawing
     * instructions.
     *
     * The parameter, `callback`, is a function with the drawing instructions
     * for the framebuffer. For example, calling `myBuffer.draw(myFunction)`
     * will call a function named `myFunction()` to draw to the framebuffer.
     * Doing so has the same effect as the following:
     *
     * ```js
     * myBuffer.begin();
     * myFunction();
     * myBuffer.end();
     * ```
     *
     * @param {Function} callback function that draws to the framebuffer.
     *
     * @example
     * // Click the canvas to display the framebuffer.
     *
     * let myBuffer;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Framebuffer object.
     *   myBuffer = createFramebuffer();
     *
     *   describe('An empty gray canvas. The canvas gets darker and a rotating, multicolor torus appears while the user presses and holds the mouse.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw to the p5.Framebuffer object.
     *   myBuffer.draw(bagel);
     *
     *   // Display the p5.Framebuffer object while
     *   // the user presses the mouse.
     *   if (mouseIsPressed === true) {
     *     image(myBuffer, -50, -50);
     *   }
     * }
     *
     * // Draw a rotating, multicolor torus.
     * function bagel() {
     *   background(50);
     *   rotateY(frameCount * 0.01);
     *   normalMaterial();
     *   torus(30);
     * }
     */
    draw(callback) {
      this.begin();
      callback();
      this.end();
    }

    /**
     * Loads the current value of each pixel in the framebuffer into its
     * <a href="#/p5.Framebuffer/pixels">pixels</a> array.
     *
     * `myBuffer.loadPixels()` must be called before reading from or writing to
     * <a href="#/p5.Framebuffer/pixels">myBuffer.pixels</a>.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   // Create a p5.Framebuffer object.
     *   let myBuffer = createFramebuffer();
     *
     *   // Load the pixels array.
     *   myBuffer.loadPixels();
     *
     *   // Get the number of pixels in the
     *   // top half of the framebuffer.
     *   let numPixels = myBuffer.pixels.length / 2;
     *
     *   // Set the framebuffer's top half to pink.
     *   for (let i = 0; i < numPixels; i += 4) {
     *     myBuffer.pixels[i] = 255;
     *     myBuffer.pixels[i + 1] = 102;
     *     myBuffer.pixels[i + 2] = 204;
     *     myBuffer.pixels[i + 3] = 255;
     *   }
     *
     *   // Update the pixels array.
     *   myBuffer.updatePixels();
     *
     *   // Draw the p5.Framebuffer object to the canvas.
     *   image(myBuffer, -50, -50);
     *
     *   describe('A pink rectangle above a gray rectangle.');
     * }
     */
    loadPixels() {
      this._update('colorTexture');
      const result = this.renderer.readFramebufferPixels(this);

      // Check if renderer returned a Promise (WebGPU) or data directly (WebGL)
      if (result && typeof result.then === 'function') {
        // WebGPU async case - return Promise
        return result.then(pixels => {
          this.pixels = pixels;
          return pixels;
        });
      } else {
        // WebGL sync case - assign directly
        this.pixels = result;
        return result;
      }
    }

    /**
     * Gets a pixel or a region of pixels from the framebuffer.
     *
     * `myBuffer.get()` is easy to use but it's not as fast as
     * <a href="#/p5.Framebuffer/pixels">myBuffer.pixels</a>. Use
     * <a href="#/p5.Framebuffer/pixels">myBuffer.pixels</a> to read many pixel
     * values.
     *
     * The version of `myBuffer.get()` with no parameters returns the entire
     * framebuffer as a a <a href="#/p5.Image">p5.Image</a> object.
     *
     * The version of `myBuffer.get()` with two parameters interprets them as
     * coordinates. It returns an array with the `[R, G, B, A]` values of the
     * pixel at the given point.
     *
     * The version of `myBuffer.get()` with four parameters interprets them as
     * coordinates and dimensions. It returns a subsection of the framebuffer as
     * a <a href="#/p5.Image">p5.Image</a> object. The first two parameters are
     * the coordinates for the upper-left corner of the subsection. The last two
     * parameters are the width and height of the subsection.
     *
     * @param  {Number} x x-coordinate of the pixel. Defaults to 0.
     * @param  {Number} y y-coordinate of the pixel. Defaults to 0.
     * @param  {Number} w width of the subsection to be returned.
     * @param  {Number} h height of the subsection to be returned.
     * @return {p5.Image} subsection as a <a href="#/p5.Image">p5.Image</a> object.
     */
    /**
     * @return {p5.Image} entire framebuffer as a <a href="#/p5.Image">p5.Image</a> object.
     */
    /**
     * @param  {Number} x
     * @param  {Number} y
     * @return {Number[]}  color of the pixel at `(x, y)` as an array of color values `[R, G, B, A]`.
     */
    get(x, y, w, h) {
      this._update('colorTexture');
      // p5._validateParameters('p5.Framebuffer.get', arguments);

      if (x === undefined && y === undefined) {
        x = 0;
        y = 0;
        w = this.width;
        h = this.height;
      } else if (w === undefined && h === undefined) {
        if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
          console.warn(
            'The x and y values passed to p5.Framebuffer.get are outside of its range and will be clamped.'
          );
          x = constrain(x, 0, this.width - 1);
          y = constrain(y, 0, this.height - 1);
        }

        return this.renderer.readFramebufferPixel(this, x * this.density, y * this.density);
      }

      x = constrain(x, 0, this.width - 1);
      y = constrain(y, 0, this.height - 1);
      w = constrain(w, 1, this.width - x);
      h = constrain(h, 1, this.height - y);

      return this.renderer.readFramebufferRegion(this, x, y, w, h);
    }

    /**
     * Updates the framebuffer with the RGBA values in the
     * <a href="#/p5.Framebuffer/pixels">pixels</a> array.
     *
     * `myBuffer.updatePixels()` only needs to be called after changing values
     * in the <a href="#/p5.Framebuffer/pixels">myBuffer.pixels</a> array. Such
     * changes can be made directly after calling
     * <a href="#/p5.Framebuffer/loadPixels">myBuffer.loadPixels()</a>.
     *
     * @method updatePixels
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   // Create a p5.Framebuffer object.
     *   let myBuffer = createFramebuffer();
     *
     *   // Load the pixels array.
     *   myBuffer.loadPixels();
     *
     *   // Get the number of pixels in the
     *   // top half of the framebuffer.
     *   let numPixels = myBuffer.pixels.length / 2;
     *
     *   // Set the framebuffer's top half to pink.
     *   for (let i = 0; i < numPixels; i += 4) {
     *     myBuffer.pixels[i] = 255;
     *     myBuffer.pixels[i + 1] = 102;
     *     myBuffer.pixels[i + 2] = 204;
     *     myBuffer.pixels[i + 3] = 255;
     *   }
     *
     *   // Update the pixels array.
     *   myBuffer.updatePixels();
     *
     *   // Draw the p5.Framebuffer object to the canvas.
     *   image(myBuffer, -50, -50);
     *
     *   describe('A pink rectangle above a gray rectangle.');
     * }
     */
    updatePixels() {
      // Let renderer handle the pixel update process
      this.renderer.updateFramebufferPixels(this);
    }
  };

  function framebuffer(p5, fn){
    /**
     * A <a href="#/p5.Camera">p5.Camera</a> attached to a
     * <a href="#/p5.Framebuffer">p5.Framebuffer</a>.
     *
     * @class p5.FramebufferCamera
     * @param {p5.Framebuffer} framebuffer The framebuffer this camera is
     * attached to
     * @private
     */
    p5.FramebufferCamera = FramebufferCamera;

    /**
     * A <a href="#/p5.Texture">p5.Texture</a> corresponding to a property of a
     * <a href="#/p5.Framebuffer">p5.Framebuffer</a>.
     *
     * @class p5.FramebufferTexture
     * @param {p5.Framebuffer} framebuffer The framebuffer represented by this
     * texture
     * @param {String} property The property of the framebuffer represented by
     * this texture, either `color` or `depth`
     * @private
     */
    p5.FramebufferTexture = FramebufferTexture;

    /**
     * A class to describe a high-performance drawing surface for textures.
     *
     * Each `p5.Framebuffer` object provides a dedicated drawing surface called
     * a *framebuffer*. They're similar to
     * <a href="#/p5.Graphics">p5.Graphics</a> objects but can run much faster.
     * Performance is improved because the framebuffer shares the same WebGL
     * context as the canvas used to create it.
     *
     * `p5.Framebuffer` objects have all the drawing features of the main
     * canvas. Drawing instructions meant for the framebuffer must be placed
     * between calls to
     * <a href="#/p5.Framebuffer/begin">myBuffer.begin()</a> and
     * <a href="#/p5.Framebuffer/end">myBuffer.end()</a>. The resulting image
     * can be applied as a texture by passing the `p5.Framebuffer` object to the
     * <a href="#/p5/texture">texture()</a> function, as in `texture(myBuffer)`.
     * It can also be displayed on the main canvas by passing it to the
     * <a href="#/p5/image">image()</a> function, as in `image(myBuffer, 0, 0)`.
     *
     * Note: <a href="#/p5/createFramebuffer">createFramebuffer()</a> is the
     * recommended way to create an instance of this class.
     *
     * @class p5.Framebuffer
     * @param {p5.Graphics|p5} target sketch instance or
     *                                <a href="#/p5.Graphics">p5.Graphics</a>
     *                                object.
     * @param {Object} [settings] configuration options.
     */
    p5.Framebuffer = Framebuffer$1;

    /**
     * An object that stores the framebuffer's color data.
     *
     * Each framebuffer uses a
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebGLTexture" target="_blank">WebGLTexture</a>
     * object internally to store its color data. The `myBuffer.color` property
     * makes it possible to pass this data directly to other functions. For
     * example, calling `texture(myBuffer.color)` or
     * `myShader.setUniform('colorTexture', myBuffer.color)`  may be helpful for
     * advanced use cases.
     *
     * Note: By default, a framebuffer's y-coordinates are flipped compared to
     * images and videos. It's easy to flip a framebuffer's y-coordinates as
     * needed when applying it as a texture. For example, calling
     * `plane(myBuffer.width, -myBuffer.height)` will flip the framebuffer.
     *
     * @property {p5.FramebufferTexture} color
     * @for p5.Framebuffer
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   // Create a p5.Framebuffer object.
     *   let myBuffer = createFramebuffer();
     *
     *   // Start drawing to the p5.Framebuffer object.
     *   myBuffer.begin();
     *
     *   triangle(-25, 25, 0, -25, 25, 25);
     *
     *   // Stop drawing to the p5.Framebuffer object.
     *   myBuffer.end();
     *
     *   // Use the p5.Framebuffer object's WebGLTexture.
     *   texture(myBuffer.color);
     *
     *   // Style the plane.
     *   noStroke();
     *
     *   // Draw the plane.
     *   plane(myBuffer.width, myBuffer.height);
     *
     *   describe('A white triangle on a gray background.');
     * }
     */

    /**
     * An object that stores the framebuffer's depth data.
     *
     * Each framebuffer uses a
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebGLTexture" target="_blank">WebGLTexture</a>
     * object internally to store its depth data. The `myBuffer.depth` property
     * makes it possible to pass this data directly to other functions. For
     * example, calling `texture(myBuffer.depth)` or
     * `myShader.setUniform('depthTexture', myBuffer.depth)`  may be helpful for
     * advanced use cases.
     *
     * Note: By default, a framebuffer's y-coordinates are flipped compared to
     * images and videos. It's easy to flip a framebuffer's y-coordinates as
     * needed when applying it as a texture. For example, calling
     * `plane(myBuffer.width, -myBuffer.height)` will flip the framebuffer.
     *
     * @property {p5.FramebufferTexture} depth
     * @for p5.Framebuffer
     *
     * @example
     * // Note: A "uniform" is a global variable within a shader program.
     *
     * // Create a string with the vertex shader program.
     * // The vertex shader is called for each vertex.
     * let vertSrc = `
     * precision highp float;
     * attribute vec3 aPosition;
     * attribute vec2 aTexCoord;
     * uniform mat4 uModelViewMatrix;
     * uniform mat4 uProjectionMatrix;
     * varying vec2 vTexCoord;
     *
     * void main() {
     *   vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0);
     *   gl_Position = uProjectionMatrix * viewModelPosition;
     *   vTexCoord = aTexCoord;
     * }
     * `;
     *
     * // Create a string with the fragment shader program.
     * // The fragment shader is called for each pixel.
     * let fragSrc = `
     * precision highp float;
     * varying vec2 vTexCoord;
     * uniform sampler2D depth;
     *
     * void main() {
     *   // Get the pixel's depth value.
     *   float depthVal = texture2D(depth, vTexCoord).r;
     *
     *   // Set the pixel's color based on its depth.
     *   gl_FragColor = mix(
     *     vec4(0., 0., 0., 1.),
     *     vec4(1., 0., 1., 1.),
     *     depthVal);
     * }
     * `;
     *
     * let myBuffer;
     * let myShader;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Framebuffer object.
     *   myBuffer = createFramebuffer();
     *
     *   // Create a p5.Shader object.
     *   myShader = createShader(vertSrc, fragSrc);
     *
     *   // Compile and apply the shader.
     *   shader(myShader);
     *
     *   describe('The shadow of a box rotates slowly against a magenta background.');
     * }
     *
     * function draw() {
     *   // Draw to the p5.Framebuffer object.
     *   myBuffer.begin();
     *   background(255);
     *   rotateX(frameCount * 0.01);
     *   box(20, 20, 80);
     *   myBuffer.end();
     *
     *   // Set the shader's depth uniform using
     *   // the framebuffer's depth texture.
     *   myShader.setUniform('depth', myBuffer.depth);
     *
     *   // Style the plane.
     *   noStroke();
     *
     *   // Draw the plane.
     *   plane(myBuffer.width, myBuffer.height);
     * }
     */

    /**
     * An array containing the color of each pixel in the framebuffer.
     *
     * <a href="#/p5.Framebuffer/loadPixels">myBuffer.loadPixels()</a> must be
     * called before accessing the `myBuffer.pixels` array.
     * <a href="#/p5.Framebuffer/updatePixels">myBuffer.updatePixels()</a>
     * must be called after any changes are made.
     *
     * Note: Updating pixels via this property is slower than drawing to the
     * framebuffer directly. Consider using a
     * <a href="#/p5.Shader">p5.Shader</a> object instead of looping over
     * `myBuffer.pixels`.
     *
     * @property {Number[]} pixels
     * @for p5.Framebuffer
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   // Create a p5.Framebuffer object.
     *   let myBuffer = createFramebuffer();
     *
     *   // Load the pixels array.
     *   myBuffer.loadPixels();
     *
     *   // Get the number of pixels in the
     *   // top half of the framebuffer.
     *   let numPixels = myBuffer.pixels.length / 2;
     *
     *   // Set the framebuffer's top half to pink.
     *   for (let i = 0; i < numPixels; i += 4) {
     *     myBuffer.pixels[i] = 255;
     *     myBuffer.pixels[i + 1] = 102;
     *     myBuffer.pixels[i + 2] = 204;
     *     myBuffer.pixels[i + 3] = 255;
     *   }
     *
     *   // Update the pixels array.
     *   myBuffer.updatePixels();
     *
     *   // Draw the p5.Framebuffer object to the canvas.
     *   image(myBuffer, -50, -50);
     *
     *   describe('A pink rectangle above a gray rectangle.');
     * }
     */

    /**
     * The current width of the framebuffer.
     *
     * @property {Number} width
     * @for p5.Framebuffer
     */

    /**
     * The current width of the framebuffer.
     *
     * @property {Number} height
     * @for p5.Framebuffer
     */
  }

  if(typeof p5 !== 'undefined'){
    framebuffer(p5);
  }

  /**
   * @module Rendering
   * @submodule Rendering
   * @for p5
   */


  let renderers;
  function rendering(p5, fn){
    // Extend additional renderers object to p5 class, new renderer can be similarly attached
    if (!p5.renderers) {
      p5.renderers = {};
    }
    renderers = p5.renderers;

    /**
     * Creates a canvas element on the web page.
     *
     * `createCanvas()` creates the main drawing canvas for a sketch. It should
     * only be called once at the beginning of <a href="#/p5/setup">setup()</a>.
     * Calling `createCanvas()` more than once causes unpredictable behavior.
     *
     * The first two parameters, `width` and `height`, are optional. They set the
     * dimensions of the canvas and the values of the
     * <a href="#/p5/width">width</a> and <a href="#/p5/height">height</a> system
     * variables. For example, calling `createCanvas(900, 500)` creates a canvas
     * that's 900×500 pixels. By default, `width` and `height` are both 100.
     *
     * The third parameter is also optional. If either of the constants `P2D` or
     * `WEBGL` is passed, as in `createCanvas(900, 500, WEBGL)`, then it will set
     * the sketch's rendering mode. If an existing
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement" target="_blank">HTMLCanvasElement</a>
     * is passed, as in `createCanvas(900, 500, myCanvas)`, then it will be used
     * by the sketch. To use `WEBGPU` mode, make sure you have the WebGPU mode addon included.
     *
     * The fourth parameter is also optional. If an existing
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement" target="_blank">HTMLCanvasElement</a>
     * is passed, as in `createCanvas(900, 500, WEBGL, myCanvas)`, then it will be
     * used by the sketch.
     *
     * Note: In WebGL mode, the canvas will use a WebGL2 context if it's supported
     * by the browser. Check the <a href="#/p5/webglVersion">webglVersion</a>
     * system variable to check what version is being used, or call
     * `setAttributes({ version: 1 })` to create a WebGL1 context.
     *
     * @method createCanvas
     * @param  {Number} [width] width of the canvas. Defaults to 100.
     * @param  {Number} [height] height of the canvas. Defaults to 100.
     * @param  {(P2D|WEBGL|P2DHDR|WEBGPU)} [renderer] either P2D, WEBGL, or WEBGPU. Defaults to `P2D`.
     * @param  {HTMLCanvasElement} [canvas] existing canvas element that should be used for the sketch.
     * @return {p5.Renderer} new `p5.Renderer` that holds the canvas.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Draw a diagonal line.
     *   line(0, 0, width, height);
     *
     *   describe('A diagonal line drawn from top-left to bottom-right on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 50);
     *
     *   background(200);
     *
     *   // Draw a diagonal line.
     *   line(0, 0, width, height);
     *
     *   describe('A diagonal line drawn from top-left to bottom-right on a gray background.');
     * }
     *
     * @example
     * // Use WebGL mode.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   background(200);
     *
     *   // Draw a diagonal line.
     *   line(-width / 2, -height / 2, width / 2, height / 2);
     *
     *   describe('A diagonal line drawn from top-left to bottom-right on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   // Create a p5.Render object.
     *   let cnv = createCanvas(50, 50);
     *
     *   // Position the canvas.
     *   cnv.position(10, 20);
     *
     *   background(200);
     *
     *   // Draw a diagonal line.
     *   line(0, 0, width, height);
     *
     *   describe('A diagonal line drawn from top-left to bottom-right on a gray background.');
     * }
     */
    /**
     * @method createCanvas
     * @param  {Number} [width]
     * @param  {Number} [height]
     * @param  {HTMLCanvasElement} [canvas]
     * @return {p5.Renderer}
     */
    fn.createCanvas = function (w, h, renderer, ...args) {
      // p5._validateParameters('createCanvas', arguments);
      //optional: renderer, otherwise defaults to p2d

      let selectedRenderer = P2D;
      // Check third argument whether it is renderer constants
      if(Reflect.ownKeys(renderers).includes(renderer)){
        selectedRenderer = renderer;
      }else {
        args.unshift(renderer);
      }

      // Init our graphics renderer
      if(this._renderer) this._renderer.remove();
      this._renderer = new renderers[selectedRenderer](this, w, h, true, ...args);
      this._defaultGraphicsCreated = true;
      this._elements.push(this._renderer);
      this._renderer._applyDefaults();

      // Make the renderer own `pixels`
      if (!Object.hasOwn(this, 'pixels')) {
        Object.defineProperty(this, 'pixels', {
          get(){
            return this._renderer?.pixels;
          }
        });
      }

      if (this._renderer.contextReady) {
        return this._renderer.contextReady.then(() => this._renderer);
      } else {
        return this._renderer;
      }
    };

    /**
     * Resizes the canvas to a given width and height.
     *
     * `resizeCanvas()` immediately clears the canvas and calls
     * <a href="#/p5/redraw">redraw()</a>. It's common to call `resizeCanvas()`
     * within the body of <a href="#/p5/windowResized">windowResized()</a> like
     * so:
     *
     * ```js
     * function windowResized() {
     *   resizeCanvas(windowWidth, windowHeight);
     * }
     * ```
     *
     * The first two parameters, `width` and `height`, set the dimensions of the
     * canvas. They also the values of the <a href="#/p5/width">width</a> and
     * <a href="#/p5/height">height</a> system variables. For example, calling
     * `resizeCanvas(300, 500)` resizes the canvas to 300×500 pixels, then sets
     * <a href="#/p5/width">width</a> to 300 and
     * <a href="#/p5/height">height</a> 500.
     *
     * The third parameter, `noRedraw`, is optional. If `true` is passed, as in
     * `resizeCanvas(300, 500, true)`, then the canvas will be canvas to 300×500
     * pixels but the <a href="#/p5/redraw">redraw()</a> function won't be called
     * immediately. By default, <a href="#/p5/redraw">redraw()</a> is called
     * immediately when `resizeCanvas()` finishes executing.
     *
     * @method resizeCanvas
     * @param  {Number} width width of the canvas.
     * @param  {Number} height height of the canvas.
     * @param  {Boolean} [noRedraw] whether to delay calling
     *                              <a href="#/p5/redraw">redraw()</a>. Defaults
     *                              to `false`.
     *
     * @example
     * // Double-click to resize the canvas.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A white circle drawn on a gray background. The canvas shrinks by half the first time the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw a circle at the center of the canvas.
     *   circle(width / 2, height / 2, 20);
     * }
     *
     * // Resize the canvas when the user double-clicks.
     * function doubleClicked() {
     *   resizeCanvas(50, 50);
     * }
     *
     * @example
     * // Resize the web browser to change the canvas size.
     *
     * function setup() {
     *   createCanvas(windowWidth, windowHeight);
     *
     *   describe('A white circle drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw a circle at the center of the canvas.
     *   circle(width / 2, height / 2, 20);
     * }
     *
     * // Always resize the canvas to fill the browser window.
     * function windowResized() {
     *   resizeCanvas(windowWidth, windowHeight);
     * }
     */
    fn.resizeCanvas = function (w, h, noRedraw) {
      // p5._validateParameters('resizeCanvas', arguments);
      if (this._renderer) {
        // Make sure width and height are updated before the renderer resizes so
        // that framebuffers updated from the resize read the correct size
        this._renderer.resize(w, h);

        if (!noRedraw) {
          this.redraw();
        }
      }
      //accessible Outputs
      if (this._addAccsOutput()) {
        this._updateAccsOutput();
      }
    };

    /**
     * Removes the default canvas.
     *
     * By default, a 100×100 pixels canvas is created without needing to call
     * <a href="#/p5/createCanvas">createCanvas()</a>. `noCanvas()` removes the
     * default canvas for sketches that don't need it.
     *
     * @method noCanvas
     *
     * @example
     * function setup() {
     *   noCanvas();
     * }
     */
    fn.noCanvas = function () {
      if (this.canvas) {
        this.canvas.parentNode.removeChild(this.canvas);
      }
    };

    /**
     * Creates a <a href="#/p5.Graphics">p5.Graphics</a> object.
     *
     * `createGraphics()` creates an offscreen drawing canvas (graphics buffer)
     * and returns it as a <a href="#/p5.Graphics">p5.Graphics</a> object. Drawing
     * to a separate graphics buffer can be helpful for performance and for
     * organizing code.
     *
     * The first two parameters, `width` and `height`, are optional. They set the
     * dimensions of the <a href="#/p5.Graphics">p5.Graphics</a> object. For
     * example, calling `createGraphics(900, 500)` creates a graphics buffer
     * that's 900×500 pixels.
     *
     * The third parameter is also optional. If either of the constants `P2D` or
     * `WEBGL` is passed, as in `createGraphics(900, 500, WEBGL)`, then it will set
     * the <a href="#/p5.Graphics">p5.Graphics</a> object's rendering mode. If an
     * existing
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement" target="_blank">HTMLCanvasElement</a>
     * is passed, as in `createGraphics(900, 500, myCanvas)`, then it will be used
     * by the graphics buffer.
     *
     * The fourth parameter is also optional. If an existing
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement" target="_blank">HTMLCanvasElement</a>
     * is passed, as in `createGraphics(900, 500, WEBGL, myCanvas)`, then it will be
     * used by the graphics buffer.
     *
     * Note: In WebGL mode, the <a href="#/p5.Graphics">p5.Graphics</a> object
     * will use a WebGL2 context if it's supported by the browser. Check the
     * <a href="#/p5/webglVersion">webglVersion</a> system variable to check what
     * version is being used, or call `setAttributes({ version: 1 })` to create a
     * WebGL1 context.
     *
     * @method createGraphics
     * @param  {Number} width width of the graphics buffer.
     * @param  {Number} height height of the graphics buffer.
     * @param  {(P2D|WEBGL)} [renderer] either P2D or WEBGL. Defaults to P2D.
     * @param  {HTMLCanvasElement} [canvas] existing canvas element that should be
     *                                      used for the graphics buffer..
     * @return {p5.Graphics} new graphics buffer.
     *
     * @example
     * //  Double-click to draw the contents of the graphics buffer.
     *
     * let pg;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create the p5.Graphics object.
     *   pg = createGraphics(50, 50);
     *
     *   // Draw to the graphics buffer.
     *   pg.background(100);
     *   pg.circle(pg.width / 2, pg.height / 2, 20);
     *
     *   describe('A gray square. A smaller, darker square with a white circle at its center appears when the user double-clicks.');
     * }
     *
     * // Display the graphics buffer when the user double-clicks.
     * function doubleClicked() {
     *   if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) {
     *     image(pg, 25, 25);
     *   }
     * }
     *
     * @example
     * //  Double-click to draw the contents of the graphics buffer.
     *
     * let pg;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create the p5.Graphics object in WebGL mode.
     *   pg = createGraphics(50, 50, WEBGL);
     *
     *   // Draw to the graphics buffer.
     *   pg.background(100);
     *   pg.lights();
     *   pg.noStroke();
     *   pg.rotateX(QUARTER_PI);
     *   pg.rotateY(QUARTER_PI);
     *   pg.torus(15, 5);
     *
     *   describe('A gray square. A smaller, darker square with a white torus at its center appears when the user double-clicks.');
     * }
     *
     * // Display the graphics buffer when the user double-clicks.
     * function doubleClicked() {
     *   if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) {
     *     image(pg, 25, 25);
     *   }
     * }
     */
    /**
     * @method createGraphics
     * @param  {Number} width
     * @param  {Number} height
     * @param  {HTMLCanvasElement} [canvas]
     * @return {p5.Graphics}
     */
    fn.createGraphics = function (w, h, ...args) {
      /**
       * args[0] is expected to be renderer
       * args[1] is expected to be canvas
       */
      if (args[0] instanceof HTMLCanvasElement) {
        args[1] = args[0];
        args[0] = P2D;
      }
      // p5._validateParameters('createGraphics', arguments);
      return new p5.Graphics(w, h, args[0], this, args[1]);
    };

    /**
     * Creates and a new <a href="#/p5.Framebuffer">p5.Framebuffer</a> object.
     *
     * <a href="#/p5.Framebuffer">p5.Framebuffer</a> objects are separate drawing
     * surfaces that can be used as textures in WebGL mode. They're similar to
     * <a href="#/p5.Graphics">p5.Graphics</a> objects and generally run much
     * faster when used as textures.
     *
     * The parameter, `options`, is optional. An object can be passed to configure
     * the <a href="#/p5.Framebuffer">p5.Framebuffer</a> object. The available
     * properties are:
     *
     * - `format`: data format of the texture, either `UNSIGNED_BYTE`, `FLOAT`, or `HALF_FLOAT`. Default is `UNSIGNED_BYTE`.
     * - `channels`: whether to store `RGB` or `RGBA` color channels. Default is to match the main canvas which is `RGBA`.
     * - `depth`: whether to include a depth buffer. Default is `true`.
     * - `depthFormat`: data format of depth information, either `UNSIGNED_INT` or `FLOAT`. Default is `FLOAT`.
     * - `stencil`: whether to include a stencil buffer for masking. `depth` must be `true` for this feature to work. Defaults to the value of `depth` which is `true`.
     * - `antialias`: whether to perform anti-aliasing. If set to `true`, as in `{ antialias: true }`, 2 samples will be used by default. The number of samples can also be set, as in `{ antialias: 4 }`. Default is to match <a href="#/p5/setAttributes">setAttributes()</a> which is `false` (`true` in Safari).
     * - `width`: width of the <a href="#/p5.Framebuffer">p5.Framebuffer</a> object. Default is to always match the main canvas width.
     * - `height`: height of the <a href="#/p5.Framebuffer">p5.Framebuffer</a> object. Default is to always match the main canvas height.
     * - `density`: pixel density of the <a href="#/p5.Framebuffer">p5.Framebuffer</a> object. Default is to always match the main canvas pixel density.
     * - `textureFiltering`: how to read values from the <a href="#/p5.Framebuffer">p5.Framebuffer</a> object. Either `LINEAR` (nearby pixels will be interpolated) or `NEAREST` (no interpolation). Generally, use `LINEAR` when using the texture as an image and `NEAREST` if reading the texture as data. Default is `LINEAR`.
     *
     * If the `width`, `height`, or `density` attributes are set, they won't automatically match the main canvas and must be changed manually.
     *
     * Note: `createFramebuffer()` can only be used in WebGL mode.
     *
     * @method createFramebuffer
     * @param {Object} [options] configuration options.
     * @param {UNSIGNED_BYTE|FLOAT|HALF_FLOAT} [options.format=UNSIGNED_BYTE] The data format of the texture.
     * @param {RGB|RGBA} [options.channels=RGBA] What color channels to include in the texture.
     * @param {Boolean} [options.depth=true] Whether to store depth information in the framebuffer.
     * @param {UNSIGNED_INT|FLOAT} [options.depthFormat=FLOAT] The format to store depth values in.
     * @param {Boolean} [options.stencil=true] Whether to include a stencil buffer (required for clipping.)
     * @param {Boolean|Number} [options.antialias] Whether to antialias when drawing to this framebuffer. Either a boolean, or the number of antialias samples to use.
     * @param {Number} [options.width] The width of the framebuffer. By default, it will match the main canvas.
     * @param {Number} [options.height] The height of the framebuffer. By default, it will match the main canvas.
     * @param {Number} [options.density] The pixel density of the framebuffer. By default, it will match the main canvas.
     * @param {LINEAR|NEAREST} [options.textureFiltering=LINEAR] The strategy used when reading values in the framebuffer in between pixels.
     * @return {p5.Framebuffer} new framebuffer.
     *
     * @example
     * let myBuffer;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create a p5.Framebuffer object.
     *   myBuffer = createFramebuffer();
     *
     *   describe('A grid of white toruses rotating against a dark gray background.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Start drawing to the p5.Framebuffer object.
     *   myBuffer.begin();
     *
     *   // Clear the drawing surface.
     *   clear();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Rotate the coordinate system.
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.01);
     *
     *   // Style the torus.
     *   noStroke();
     *
     *   // Draw the torus.
     *   torus(20);
     *
     *   // Stop drawing to the p5.Framebuffer object.
     *   myBuffer.end();
     *
     *   // Iterate from left to right.
     *   for (let x = -50; x < 50; x += 25) {
     *     // Iterate from top to bottom.
     *     for (let y = -50; y < 50; y += 25) {
     *       // Draw the p5.Framebuffer object to the canvas.
     *       image(myBuffer, x, y, 25, 25);
     *     }
     *   }
     * }
     *
     * @example
     * let myBuffer;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create an options object.
     *   let options = { width: 25, height: 25 };
     *
     *   // Create a p5.Framebuffer object.
     *   // Use options for configuration.
     *   myBuffer = createFramebuffer(options);
     *
     *   describe('A grid of white toruses rotating against a dark gray background.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Start drawing to the p5.Framebuffer object.
     *   myBuffer.begin();
     *
     *   // Clear the drawing surface.
     *   clear();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Rotate the coordinate system.
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.01);
     *
     *   // Style the torus.
     *   noStroke();
     *
     *   // Draw the torus.
     *   torus(5, 2.5);
     *
     *   // Stop drawing to the p5.Framebuffer object.
     *   myBuffer.end();
     *
     *   // Iterate from left to right.
     *   for (let x = -50; x < 50; x += 25) {
     *     // Iterate from top to bottom.
     *     for (let y = -50; y < 50; y += 25) {
     *       // Draw the p5.Framebuffer object to the canvas.
     *       image(myBuffer, x, y);
     *     }
     *   }
     * }
     */
    fn.createFramebuffer = function (options) {
      return new Framebuffer$1(this._renderer, options);
    };

    /**
     * Clears the depth buffer in WebGL mode.
     *
     * `clearDepth()` clears information about how far objects are from the camera
     * in 3D space. This information is stored in an object called the
     * *depth buffer*. Clearing the depth buffer ensures new objects aren't drawn
     * behind old ones. Doing so can be useful for feedback effects in which the
     * previous frame serves as the background for the current frame.
     *
     * The parameter, `depth`, is optional. If a number is passed, as in
     * `clearDepth(0.5)`, it determines the range of objects to clear from the
     * depth buffer. 0 doesn't clear any depth information, 0.5 clears depth
     * information halfway between the near and far clipping planes, and 1 clears
     * depth information all the way to the far clipping plane. By default,
     * `depth` is 1.
     *
     * Note: `clearDepth()` can only be used in WebGL mode.
     *
     * @method clearDepth
     * @param {Number} [depth] amount of the depth buffer to clear between 0
     *                         (none) and 1 (far clipping plane). Defaults to 1.
     *
     * @example
     * let previous;
     * let current;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the p5.Framebuffer objects.
     *   previous = createFramebuffer({ format: FLOAT });
     *   current = createFramebuffer({ format: FLOAT });
     *
     *   describe(
     *     'A multicolor box drifts from side to side on a white background. It leaves a trail that fades over time.'
     *   );
     * }
     *
     * function draw() {
     *   // Swap the previous p5.Framebuffer and the
     *   // current one so it can be used as a texture.
     *   [previous, current] = [current, previous];
     *
     *   // Start drawing to the current p5.Framebuffer.
     *   current.begin();
     *
     *   // Paint the background.
     *   background(255);
     *
     *   // Draw the previous p5.Framebuffer.
     *   // Clear the depth buffer so the previous
     *   // frame doesn't block the current one.
     *   push();
     *   tint(255, 250);
     *   image(previous, -50, -50);
     *   clearDepth();
     *   pop();
     *
     *   // Draw the box on top of the previous frame.
     *   push();
     *   let x = 25 * sin(frameCount * 0.01);
     *   let y = 25 * sin(frameCount * 0.02);
     *   translate(x, y, 0);
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.01);
     *   normalMaterial();
     *   box(12);
     *   pop();
     *
     *   // Stop drawing to the current p5.Framebuffer.
     *   current.end();
     *
     *   // Display the current p5.Framebuffer.
     *   image(current, -50, -50);
     * }
     */
    fn.clearDepth = function (depth) {
      this._assert3d('clearDepth');
      this._renderer.clearDepth(depth);
    };

    /**
     * A system variable that provides direct access to the sketch's
     * `&lt;canvas&gt;` element.
     *
     * The `&lt;canvas&gt;` element provides many specialized features that aren't
     * included in the p5.js library. The `drawingContext` system variable
     * provides access to these features by exposing the sketch's
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D">CanvasRenderingContext2D</a>
     * object.
     *
     * @property {CanvasRenderingContext2D|WebGLRenderingContext|WebGL2RenderingContext} drawingContext
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the circle using shadows.
     *   drawingContext.shadowOffsetX = 5;
     *   drawingContext.shadowOffsetY = -5;
     *   drawingContext.shadowBlur = 10;
     *   drawingContext.shadowColor = 'black';
     *
     *   // Draw the circle.
     *   circle(50, 50, 40);
     *
     *   describe("A white circle on a gray background. The circle's edges are shadowy.");
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background('skyblue');
     *
     *   // Style the circle using a color gradient.
     *   let myGradient = drawingContext.createRadialGradient(50, 50, 3, 50, 50, 40);
     *   myGradient.addColorStop(0, 'yellow');
     *   myGradient.addColorStop(0.6, 'orangered');
     *   myGradient.addColorStop(1, 'yellow');
     *   drawingContext.fillStyle = myGradient;
     *   drawingContext.strokeStyle = 'rgba(0, 0, 0, 0)';
     *
     *   // Draw the circle.
     *   circle(50, 50, 40);
     *
     *   describe('A fiery sun drawn on a light blue background.');
     * }
     */
  }

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

  var filterBaseFrag = "precision highp float;\n\nuniform sampler2D tex0;\nuniform vec2 canvasSize;\nuniform vec2 texelSize;\n\nIN vec2 vTexCoord;\n\nstruct FilterInputs {\n  vec2 texCoord;\n  vec2 canvasSize;\n  vec2 texelSize;\n};\n\nvoid main(void) {\n  FilterInputs inputs;\n  inputs.texCoord = vTexCoord;\n  inputs.canvasSize = canvasSize;\n  inputs.texelSize = texelSize;\n  OUT_COLOR = HOOK_getColor(inputs, tex0);\n  OUT_COLOR.rgb *= outColor.a;\n}\n";

  var filterBaseVert = "precision highp int;\n\nuniform mat4 uModelViewMatrix;\nuniform mat4 uProjectionMatrix;\n\nIN vec3 aPosition;\nIN vec2 aTexCoord;\nOUT vec2 vTexCoord;\n\nvoid main() {\n  // transferring texcoords for the frag shader\n  vTexCoord = aTexCoord;\n\n  // copy position with a fourth coordinate for projection (1.0 is normal)\n  vec4 positionVec4 = vec4(aPosition, 1.0);\n\n  // project to 3D space\n  gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;\n}\n";

  var webgl2CompatibilityShader = "#ifdef WEBGL2\n\n#define IN in\n#define OUT out\n\n#ifdef FRAGMENT_SHADER\nout vec4 outColor;\n#define OUT_COLOR outColor\n#endif\n#define TEXTURE texture\n\n#else\n\n#ifdef FRAGMENT_SHADER\n#define IN varying\n#else\n#define IN attribute\n#endif\n#define OUT varying\n#define TEXTURE texture2D\n\n#ifdef FRAGMENT_SHADER\n#define OUT_COLOR gl_FragColor\n#endif\n\n#endif\n\nvec4 getTexture(in sampler2D content, vec2 coord) {\n  vec4 color = TEXTURE(content, coord);\n  if (color.a > 0.) color.rgb /= color.a;\n  return color;\n}\n";

  /////////////////////
  // Enums for nodes //
  /////////////////////
  const NodeType = {
    OPERATION: 'operation',
    LITERAL: 'literal',
    VARIABLE: 'variable',
    CONSTANT: 'constant',
    STRUCT: 'struct',
    PHI: 'phi',
    STATEMENT: 'statement',
    ASSIGNMENT: 'assignment',
  };
  const NodeTypeToName = Object.fromEntries(
    Object.entries(NodeType).map(([key, val]) => [val, key])
  );
  const NodeTypeRequiredFields = {
    [NodeType.OPERATION]: ["opCode", "dependsOn", "dimension", "baseType"],
    [NodeType.LITERAL]: ["value", "dimension", "baseType"],
    [NodeType.VARIABLE]: ["identifier", "dimension", "baseType"],
    [NodeType.CONSTANT]: ["value", "dimension", "baseType"],
    [NodeType.STRUCT]: [""],
    [NodeType.PHI]: ["dependsOn", "phiBlocks", "dimension", "baseType"],
    [NodeType.STATEMENT]: ["statementType"],
    [NodeType.ASSIGNMENT]: ["dependsOn"]
  };
  const StatementType = {
    DISCARD: 'discard',
    BREAK: 'break',
    EARLY_RETURN: 'early_return',
    EXPRESSION: 'expression', // Used when we want to output a single expression as a statement, e.g. a for loop condition
    EMPTY: 'empty', // Used for empty statements like ; in for loops
  };
  const BaseType = {
    FLOAT: "float",
    INT: "int",
    BOOL: "bool",
    MAT: "mat",
    DEFER: "defer",
    SAMPLER2D: "sampler2D",
    SAMPLER: "sampler",
  };
  const BasePriority = {
    [BaseType.FLOAT]: 3,
    [BaseType.INT]: 2,
    [BaseType.BOOL]: 1,
    [BaseType.MAT]: 0,
    [BaseType.DEFER]: -1,
    [BaseType.SAMPLER2D]: -10,
    [BaseType.SAMPLER]: -11,
  };
  const DataType = {
    float1: { fnName: "float", baseType: BaseType.FLOAT, dimension:1, priority: 3,  },
    float2: { fnName: "vec2", baseType: BaseType.FLOAT, dimension:2, priority: 3,  },
    float3: { fnName: "vec3", baseType: BaseType.FLOAT, dimension:3, priority: 3,  },
    float4: { fnName: "vec4", baseType: BaseType.FLOAT, dimension:4, priority: 3,  },
    int1: { fnName: "int", baseType: BaseType.INT, dimension:1, priority: 2,  },
    int2: { fnName: "ivec2", baseType: BaseType.INT, dimension:2, priority: 2,  },
    int3: { fnName: "ivec3", baseType: BaseType.INT, dimension:3, priority: 2,  },
    int4: { fnName: "ivec4", baseType: BaseType.INT, dimension:4, priority: 2,  },
    bool1: { fnName: "bool", baseType: BaseType.BOOL, dimension:1, priority: 1,  },
    bool2: { fnName: "bvec2", baseType: BaseType.BOOL, dimension:2, priority: 1,  },
    bool3: { fnName: "bvec3", baseType: BaseType.BOOL, dimension:3, priority: 1,  },
    bool4: { fnName: "bvec4", baseType: BaseType.BOOL, dimension:4, priority: 1,  },
    mat2: { fnName: "mat2x2", baseType: BaseType.MAT, dimension:2, priority: 0,  },
    mat3: { fnName: "mat3x3", baseType: BaseType.MAT, dimension:3, priority: 0,  },
    mat4: { fnName: "mat4x4", baseType: BaseType.MAT, dimension:4, priority: 0,  },
    defer: { fnName:  null, baseType: BaseType.DEFER, dimension: null, priority: -1 },
    sampler2D: { fnName: "sampler2D", baseType: BaseType.SAMPLER2D, dimension: 1, priority: -10 },
    sampler: { fnName: "sampler", baseType: BaseType.SAMPLER, dimension: 1, priority: -11 },
  };
  const structType = function (hookType) {
    let T = hookType.type === undefined ? hookType : hookType.type;
    const structType = {
      name: hookType.name,
      properties: [],
      typeName: T.typeName,
    };
    // TODO: handle struct properties that are themselves structs
    for (const prop of T.properties) {
      const propType = prop.type.dataType;
      structType.properties.push(
        {name: prop.name, dataType: propType }
      );
    }
    return structType;
  };
  function isStructType(typeInfo) {
    return !!(typeInfo && typeInfo.properties);
  }
  const GenType = {
    FLOAT: { baseType: BaseType.FLOAT, dimension: null, priority: 3 },
    INT: { baseType: BaseType.INT, dimension: null, priority: 2 },
    BOOL: { baseType: BaseType.BOOL, dimension: null, priority: 1 },
  };
  function typeEquals(nodeA, nodeB) {
    return (nodeA.dimension === nodeB.dimension) && (nodeA.baseType === nodeB.baseType);
  }
  const TypeInfoFromGLSLName = Object.fromEntries(
    Object.values(DataType)
      .filter(info => info.fnName !== null)
      .map(info => [info.fnName, info])
  );
  const OpCode = {
    Binary: {
      ADD: 0,
      SUBTRACT: 1,
      MULTIPLY: 2,
      DIVIDE: 3,
      MODULO: 4,
      EQUAL: 5,
      NOT_EQUAL: 6,
      GREATER_THAN: 7,
      GREATER_EQUAL: 8,
      LESS_THAN: 9,
      LESS_EQUAL: 10,
      LOGICAL_AND: 11,
      LOGICAL_OR: 12,
      MEMBER_ACCESS: 13,
    },
    Unary: {
      LOGICAL_NOT: 100,
      NEGATE: 101,
      PLUS: 102,
      SWIZZLE: 103,
    },
    Nary: {
      FUNCTION_CALL: 200,
      CONSTRUCTOR: 201,
    }};
  const OperatorTable = [
    { arity: "unary", name: "not", symbol: "!", opCode: OpCode.Unary.LOGICAL_NOT },
    { arity: "unary", name: "neg", symbol: "-", opCode: OpCode.Unary.NEGATE },
    { arity: "unary", name: "plus", symbol: "+", opCode: OpCode.Unary.PLUS },
    { arity: "binary", name: "add", symbol: "+", opCode: OpCode.Binary.ADD },
    { arity: "binary", name: "sub", symbol: "-", opCode: OpCode.Binary.SUBTRACT },
    { arity: "binary", name: "mult", symbol: "*", opCode: OpCode.Binary.MULTIPLY },
    { arity: "binary", name: "div", symbol: "/", opCode: OpCode.Binary.DIVIDE },
    { arity: "binary", name: "mod", symbol: "%", opCode: OpCode.Binary.MODULO },
    { arity: "binary", name: "equalTo", symbol: "==", opCode: OpCode.Binary.EQUAL },
    { arity: "binary", name: "notEqual", symbol: "!=", opCode: OpCode.Binary.NOT_EQUAL },
    { arity: "binary", name: "greaterThan", symbol: ">", opCode: OpCode.Binary.GREATER_THAN },
    { arity: "binary", name: "greaterEqual", symbol: ">=", opCode: OpCode.Binary.GREATER_EQUAL },
    { arity: "binary", name: "lessThan", symbol: "<", opCode: OpCode.Binary.LESS_THAN },
    { arity: "binary", name: "lessEqual", symbol: "<=", opCode: OpCode.Binary.LESS_EQUAL },
    { arity: "binary", name: "and", symbol: "&&", opCode: OpCode.Binary.LOGICAL_AND },
    { arity: "binary", name: "or", symbol: "||", opCode: OpCode.Binary.LOGICAL_OR },
  ];
  // export const SymbolToOpCode = {};
  const OpCodeToSymbol = {};
  const UnarySymbolToName = {};
  for (const { symbol, opCode, name, arity } of OperatorTable) {
    // SymbolToOpCode[symbol] = opCode;
    OpCodeToSymbol[opCode] = symbol;
    if (arity === 'unary') {
      UnarySymbolToName[symbol] = name;
    }
  }
  const BlockType = {
    GLOBAL: 'global',
    FUNCTION: 'function',
    BRANCH: 'branch',
    IF_COND: 'if_cond',
    IF_BODY: 'if_body',
    ELSE_COND: 'else_cond',
    SCOPE_START: 'scope_start',
    SCOPE_END: 'scope_end',
    FOR: 'for',
    MERGE: 'merge',
    DEFAULT: 'default',
  };
  Object.fromEntries(
    Object.entries(BlockType).map(([key, val]) => [val, key])
  );

  function internalError(errorMessage) {
      const prefixedMessage = `[p5.strands internal error]: ${errorMessage}`; 
      throw new Error(prefixedMessage);
  }

  function userError(errorType, errorMessage) {
      const prefixedMessage = `[p5.strands ${errorType}]: ${errorMessage}`;
      throw new Error(prefixedMessage);
  }

  /////////////////////////////////
  // Public functions for strands runtime
  /////////////////////////////////

  function createDirectedAcyclicGraph() {
    const graph = {
      nextID: 0,
      cache: new Map(),
      nodeTypes: [],
      baseTypes: [],
      dimensions: [],
      opCodes: [],
      values: [],
      identifiers: [],
      phiBlocks: [],
      dependsOn: [],
      usedBy: [],
      statementTypes: [],
      swizzles: [],
    };

    return graph;
  }

  function getOrCreateNode(graph, node) {
    // const key = getNodeKey(node);
    // const existing = graph.cache.get(key);

    // if (existing !== undefined) {
      // return existing;
    // } else {
      const id = createNode(graph, node);
      // graph.cache.set(key, id);
      return id;
    // }
  }

  function createNodeData(data = {}) {
    const node = {
      nodeType: data.nodeType ?? null,
      baseType: data.baseType ?? null,
      dimension: data.dimension ?? null,
      opCode: data.opCode ?? null,
      value: data.value ?? null,
      identifier: data.identifier ?? null,
      statementType: data.statementType ?? null,
      swizzle: data.swizzle ?? null,
      dependsOn: Array.isArray(data.dependsOn) ? data.dependsOn : [],
      usedBy: Array.isArray(data.usedBy) ? data.usedBy : [],
      phiBlocks: Array.isArray(data.phiBlocks) ? data.phiBlocks : [],
    };
    validateNode(node);
    return node;
  }

  function getNodeDataFromID(graph, id) {
    return {
      id,
      nodeType: graph.nodeTypes[id],
      opCode: graph.opCodes[id],
      value: graph.values[id],
      identifier: graph.identifiers[id],
      dependsOn: graph.dependsOn[id],
      usedBy: graph.usedBy[id],
      phiBlocks: graph.phiBlocks[id],
      dimension: graph.dimensions[id],
      baseType: graph.baseTypes[id],
      statementType: graph.statementTypes[id],
      swizzle: graph.swizzles[id],
    }
  }

  function extractNodeTypeInfo(dag, nodeID) {
    return {
      baseType: dag.baseTypes[nodeID],
      dimension: dag.dimensions[nodeID],
      priority: BasePriority[dag.baseTypes[nodeID]],
    };
  }

  /////////////////////////////////
  // Private functions
  /////////////////////////////////
  function createNode(graph, node) {
    const id = graph.nextID++;
    graph.nodeTypes[id] = node.nodeType;
    graph.opCodes[id] = node.opCode;
    graph.values[id] = node.value;
    graph.identifiers[id] = node.identifier;
    graph.dependsOn[id] = node.dependsOn.slice();
    graph.usedBy[id] = node.usedBy;
    graph.phiBlocks[id] = node.phiBlocks.slice();
    graph.baseTypes[id] = node.baseType;
    graph.dimensions[id] = node.dimension;
    graph.statementTypes[id] = node.statementType;
    graph.swizzles[id] = node.swizzle;

    for (const dep of node.dependsOn) {
      if (!Array.isArray(graph.usedBy[dep])) {
        graph.usedBy[dep] = [];
      }
      graph.usedBy[dep].push(id);
    }
    return id;
  }

  function validateNode(node){
    const nodeType = node.nodeType;
    const requiredFields = NodeTypeRequiredFields[nodeType];
    if (requiredFields.length === 2) {
      internalError(`Required fields for node type '${NodeTypeToName[nodeType]}' not defined. Please add them to the utils.js file in p5.strands!`);
    }
    const missingFields = [];
    for (const field of requiredFields) {
      if (node[field] === null) {
        missingFields.push(field);
      }
    }
    if (node.dependsOn?.some(v => v === undefined)) {
      throw new Error('Undefined dependency!');
    }
    if (missingFields.length > 0) {
      internalError(`Missing fields ${missingFields.join(', ')} for a node type '${NodeTypeToName[nodeType]}'.`);
    }
  }

  // Todo: remove edges to simplify. Block order is always ordered already.

  function createControlFlowGraph() {
    return {
      // graph structure
      blockTypes: [],
      incomingEdges: [],
      outgoingEdges: [],
      blockInstructions: [],
      // runtime data for constructing graph
      nextID: 0,
      blockStack: [],
      blockOrder: [],
      blockConditions: {},
      currentBlock: -1,
    };
  }

  function pushBlock(graph, blockID) {
    graph.blockStack.push(blockID);
    graph.blockOrder.push(blockID);
    graph.currentBlock = blockID;
  }

  function popBlock(graph) {
    graph.blockStack.pop();
    const len = graph.blockStack.length;
    graph.currentBlock = graph.blockStack[len-1];
  }

  function pushBlockForModification(graph, blockID) {
    graph.blockStack.push(blockID);
    graph.currentBlock = blockID;
  }

  function createBasicBlock(graph, blockType) {
    const id = graph.nextID++;
    graph.blockTypes[id] = blockType;
    graph.incomingEdges[id] = [];
    graph.outgoingEdges[id] = [];
    graph.blockInstructions[id]= [];
    return id;
  }

  function addEdge(graph, from, to) {
    graph.outgoingEdges[from].push(to);
    graph.incomingEdges[to].push(from);
  }

  function recordInBasicBlock(graph, blockID, nodeID) {
    if (nodeID === undefined) {
      internalError('undefined nodeID in `recordInBasicBlock()`');
    }
    if (blockID === undefined) {
      internalError('undefined blockID in `recordInBasicBlock()');
    }
    graph.blockInstructions[blockID] = graph.blockInstructions[blockID] || [];
    graph.blockInstructions[blockID].push(nodeID);
  }

  function sortCFG(adjacencyList, start) {
    const visited = new Set();
    const postOrder = [];
    function dfs(v) {
      if (visited.has(v)) {
        return;
      }
      visited.add(v);
      for (let w of adjacencyList[v].sort((a, b) => b-a) || []) {
        dfs(w);
      }
      postOrder.push(v);
    }

    dfs(start);
    return postOrder.reverse();
  }

  class StrandsNode {
    constructor(id, dimension, strandsContext) {
      this.id = id;
      this.strandsContext = strandsContext;
      this.dimension = dimension;
      this.structProperties = null;
      this.isStrandsNode = true;

      // Store original identifier for varying variables
      const dag = this.strandsContext.dag;
      const nodeData = getNodeDataFromID(dag, this.id);
      if (nodeData && nodeData.identifier) {
        this._originalIdentifier = nodeData.identifier;
      }
      if (nodeData) {
        this._originalBaseType = nodeData.baseType;
        this._originalDimension = nodeData.dimension;
      }
    }
    withStructProperties(properties) {
      this.structProperties = properties;
      return this;
    }
    copy() {
      return createStrandsNode(this.id, this.dimension, this.strandsContext);
    }
    typeInfo() {
      return {
        baseType: this._originalBaseType || BaseType.FLOAT,
        dimension: this.dimension
      };
    }
    bridge(value) {
      const { dag, cfg } = this.strandsContext;
      const orig = getNodeDataFromID(dag, this.id);
      const baseType = orig?.baseType ?? BaseType.FLOAT;

      let newValueID;
      if (value instanceof StrandsNode) {
        newValueID = value.id;
      } else {
        const newVal = primitiveConstructorNode(
          this.strandsContext,
          { baseType, dimension: this.dimension },
          value
        );
        newValueID = newVal.id;
      }

      // For varying variables, we need both assignment generation AND a way to reference by identifier
      if (this._originalIdentifier) {
        // Create a variable node for the target (the varying variable)
        const { id: targetVarID } = variableNode(
          this.strandsContext,
          { baseType: this._originalBaseType, dimension: this._originalDimension },
          this._originalIdentifier
        );

        // Create assignment node for GLSL generation
        const assignmentNode = createNodeData({
          nodeType: NodeType.ASSIGNMENT,
          dependsOn: [targetVarID, newValueID],
          phiBlocks: []
        });
        const assignmentID = getOrCreateNode(dag, assignmentNode);
        recordInBasicBlock(cfg, cfg.currentBlock, assignmentID);

        // Track for global assignments processing
        this.strandsContext.globalAssignments.push(assignmentID);

        // Simply update this node to be a variable node with the identifier
        // This ensures it always generates the variable name in expressions
        const variableNodeData = createNodeData({
          nodeType: NodeType.VARIABLE,
          baseType: this._originalBaseType,
          dimension: this._originalDimension,
          identifier: this._originalIdentifier
        });
        const variableID = getOrCreateNode(dag, variableNodeData);

        this.id = variableID; // Point to the variable node for expression generation
      } else {
        this.id = newValueID; // For non-varying variables, just update to new value
      }

      return this;
    }
    bridgeSwizzle(swizzlePattern, value) {
      const { dag, cfg } = this.strandsContext;
      const orig = getNodeDataFromID(dag, this.id);
      const baseType = orig?.baseType ?? BaseType.FLOAT;

      let newValueID;
      if (value instanceof StrandsNode) {
        newValueID = value.id;
      } else {
        const newVal = primitiveConstructorNode(
          this.strandsContext,
          { baseType, dimension: this.dimension },
          value
        );
        newValueID = newVal.id;
      }

      // For varying variables, create swizzle assignment
      if (this._originalIdentifier) {
        // Create a variable node for the target with swizzle
        const { id: targetVarID } = variableNode(
          this.strandsContext,
          { baseType: this._originalBaseType, dimension: this._originalDimension },
          this._originalIdentifier
        );

        // Create a swizzle node for the target (myVarying.xyz)
        const swizzleNode = createNodeData({
          nodeType: NodeType.OPERATION,
          opCode: OpCode.Unary.SWIZZLE,
          baseType: this._originalBaseType,
          dimension: swizzlePattern.length, // xyz = 3, xy = 2, etc.
          swizzle: swizzlePattern,
          dependsOn: [targetVarID]
        });
        const swizzleID = getOrCreateNode(dag, swizzleNode);

        // Create assignment node: myVarying.xyz = value
        const assignmentNode = createNodeData({
          nodeType: NodeType.ASSIGNMENT,
          dependsOn: [swizzleID, newValueID],
          phiBlocks: []
        });
        const assignmentID = getOrCreateNode(dag, assignmentNode);
        recordInBasicBlock(cfg, cfg.currentBlock, assignmentID);

        // Track for global assignments processing in the current hook context
        this.strandsContext.globalAssignments.push(assignmentID);

        // Simply update this node to be a variable node with the identifier
        // This ensures it always generates the variable name in expressions
        const variableNodeData = createNodeData({
          nodeType: NodeType.VARIABLE,
          baseType: this._originalBaseType,
          dimension: this._originalDimension,
          identifier: this._originalIdentifier
        });
        const variableID = getOrCreateNode(dag, variableNodeData);

        this.id = variableID; // Point to the variable node, not the assignment node
      } else {
        this.id = newValueID; // For non-varying variables, just update to new value
      }

      return this;
    }
    getValue() {
      if (this._originalIdentifier) {
        const { id, dimension } = variableNode(
          this.strandsContext,
          { baseType: this._originalBaseType, dimension: this._originalDimension },
          this._originalIdentifier
        );
        return createStrandsNode(id, dimension, this.strandsContext);
      }

      return this;
    }
  }
  function createStrandsNode(id, dimension, strandsContext, onRebind) {
    return new Proxy(
      new StrandsNode(id, dimension, strandsContext),
      swizzleTrap(id, dimension, strandsContext, onRebind)
    );
  }

  // Need the .js extension because we also import this from a Node script.
  // Try to keep this file minimal because of that.

  // GLSL Built in functions
  // https://docs.gl/el3/abs
  const builtInGLSLFunctions = {
    //////////// Trigonometry //////////
    acos: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
    acosh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
    asin: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
    asinh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
    atan: [
      { params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true},
      { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true},
    ],
    atanh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
    cos: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
    cosh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
    degrees: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
    radians: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
    sin: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT , isp5Function: true}],
    sinh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
    tan: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
    tanh: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],

    ////////// Mathematics //////////
    abs: [
      { params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true},
      { params: [GenType.FLOAT], returnType: GenType.INT, isp5Function: true}
    ],
    ceil: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
    clamp: [
      { params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false},
      { params: [GenType.FLOAT,DataType.float1,DataType.float1], returnType: GenType.FLOAT, isp5Function: false},
      { params: [GenType.INT, GenType.INT, GenType.INT], returnType: GenType.INT, isp5Function: false},
      { params: [GenType.INT, DataType.int1, DataType.int1], returnType: GenType.INT, isp5Function: false},
    ],
    dFdx: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
    dFdy: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
    exp: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
    exp2: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
    floor: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
    fma: [{ params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
    fract: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
    fwidth: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
    inversesqrt: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
    // "isinf": [{}],
    // "isnan": [{}],
    log: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
    log2: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
    max: [
      { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true},
      { params: [GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: true},
      { params: [GenType.INT, GenType.INT], returnType: GenType.INT, isp5Function: true},
      { params: [GenType.INT, DataType.int1], returnType: GenType.INT, isp5Function: true},
    ],
    min: [
      { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true},
      { params: [GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: true},
      { params: [GenType.INT, GenType.INT], returnType: GenType.INT, isp5Function: true},
      { params: [GenType.INT, DataType.int1], returnType: GenType.INT, isp5Function: true},
    ],
    mix: [
      { params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false},
      { params: [GenType.FLOAT, GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: false},
      { params: [GenType.FLOAT, GenType.FLOAT, GenType.BOOL], returnType: GenType.FLOAT, isp5Function: false},
    ],
    mod: [
      { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true},
      { params: [GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: true},
    ],
    // "modf": [{}],
    pow: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
    round: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
    roundEven: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
    sign: [
      { params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false},
      { params: [GenType.INT], returnType: GenType.INT, isp5Function: false},
    ],
    smoothstep: [
      { params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false},
      { params: [ DataType.float1,DataType.float1, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false},
    ],
    sqrt: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
    step: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
    trunc: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],

    ////////// Vector //////////
    cross: [{ params: [DataType.float3, DataType.float3], returnType: DataType.float3, isp5Function: true}],
    distance: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType:DataType.float1, isp5Function: true}],
    dot: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType:DataType.float1, isp5Function: true}],
    equal: [
      { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.BOOL, isp5Function: false},
      { params: [GenType.INT, GenType.INT], returnType: GenType.BOOL, isp5Function: false},
      { params: [GenType.BOOL, GenType.BOOL], returnType: GenType.BOOL, isp5Function: false},
    ],
    faceforward: [{ params: [GenType.FLOAT, GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
    length: [{ params: [GenType.FLOAT], returnType:DataType.float1, isp5Function: false}],
    normalize: [{ params: [GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: true}],
    notEqual: [
      { params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.BOOL, isp5Function: false},
      { params: [GenType.INT, GenType.INT], returnType: GenType.BOOL, isp5Function: false},
      { params: [GenType.BOOL, GenType.BOOL], returnType: GenType.BOOL, isp5Function: false},
    ],
    reflect: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}],
    refract: [{ params: [GenType.FLOAT, GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: false}],
  };

  const strandsBuiltinFunctions = {
    ...builtInGLSLFunctions,
  };

  //////////////////////////////////////////////
  // Builders for node graphs
  //////////////////////////////////////////////
  function scalarLiteralNode(strandsContext, typeInfo, value) {
    const { cfg, dag } = strandsContext;
    let { dimension, baseType } = typeInfo;
    if (dimension !== 1) {
      internalError('Created a scalar literal node with dimension > 1.');
    }
    const nodeData = createNodeData({
      nodeType: NodeType.LITERAL,
      dimension,
      baseType,
      value
    });
    const id = getOrCreateNode(dag, nodeData);
    recordInBasicBlock(cfg, cfg.currentBlock, id);
    return { id, dimension };
  }

  function variableNode(strandsContext, typeInfo, identifier) {
    const { cfg, dag } = strandsContext;
    const { dimension, baseType } = typeInfo;
    const nodeData = createNodeData({
      nodeType: NodeType.VARIABLE,
      dimension,
      baseType,
      identifier
    });
    const id = getOrCreateNode(dag, nodeData);
    recordInBasicBlock(cfg, cfg.currentBlock, id);
    return { id, dimension };
  }

  function unaryOpNode(strandsContext, nodeOrValue, opCode) {
    const { dag, cfg } = strandsContext;
    let dependsOn;
    let node;
    if (nodeOrValue instanceof StrandsNode) {
      node = nodeOrValue;
    } else {
      const { id, dimension } = primitiveConstructorNode(strandsContext, { baseType: BaseType.FLOAT, dimension: null }, nodeOrValue);
      node = createStrandsNode(id, dimension, strandsContext);
    }
    dependsOn = [node.id];
    const nodeData = createNodeData({
      nodeType: NodeType.OPERATION,
      opCode,
      dependsOn,
      baseType: dag.baseTypes[node.id],
      dimension: node.dimension
    });
    const id = getOrCreateNode(dag, nodeData);
    recordInBasicBlock(cfg, cfg.currentBlock, id);
    return { id, dimension: node.dimension };
  }

  function binaryOpNode(strandsContext, leftStrandsNode, rightArg, opCode) {
    const { dag, cfg } = strandsContext;
    // Construct a node for right if its just an array or number etc.
    let rightStrandsNode;
    if (rightArg[0] instanceof StrandsNode && rightArg.length === 1) {
      rightStrandsNode = rightArg[0];
    } else {
      const { id, dimension } = primitiveConstructorNode(strandsContext, { baseType: BaseType.FLOAT, dimension: null }, rightArg);
      rightStrandsNode = createStrandsNode(id, dimension, strandsContext);
    }
    let finalLeftNodeID = leftStrandsNode.id;
    let finalRightNodeID = rightStrandsNode.id;

    // Check if we have to cast either node
    const leftType = extractNodeTypeInfo(dag, leftStrandsNode.id);
    const rightType = extractNodeTypeInfo(dag, rightStrandsNode.id);
    const cast = { node: null, toType: leftType };
    const bothDeferred = leftType.baseType === rightType.baseType && leftType.baseType === BaseType.DEFER;
    if (bothDeferred) {
      cast.toType.baseType = BaseType.FLOAT;
      if (leftType.dimension === rightType.dimension) {
        cast.toType.dimension = leftType.dimension;
      }
      else if (leftType.dimension === 1 && rightType.dimension > 1) {
        cast.toType.dimension = rightType.dimension;
      }
      else if (rightType.dimension === 1 && leftType.dimension > 1) {
        cast.toType.dimension = leftType.dimension;
      }
      else {
        userError("type error", `You have tried to perform a binary operation:\n`+
          `${leftType.baseType+leftType.dimension} ${OpCodeToSymbol[opCode]} ${rightType.baseType+rightType.dimension}\n` +
          `It's only possible to operate on two nodes with the same dimension, or a scalar value and a vector.`
        );
      }
      const l = primitiveConstructorNode(strandsContext, cast.toType, leftStrandsNode);
      const r = primitiveConstructorNode(strandsContext, cast.toType, rightStrandsNode);
      finalLeftNodeID = l.id;
      finalRightNodeID = r.id;
    }
    else if (leftType.baseType !== rightType.baseType ||
      leftType.dimension !== rightType.dimension) {

      if (leftType.dimension === 1 && rightType.dimension > 1) {
        cast.node = leftStrandsNode;
        cast.toType = rightType;
      }
      else if (rightType.dimension === 1 && leftType.dimension > 1) {
        cast.node = rightStrandsNode;
        cast.toType = leftType;
      }
      else if (leftType.priority > rightType.priority) {
        // e.g. op(float vector, int vector): cast priority is float > int > bool
        cast.node = rightStrandsNode;
        cast.toType = leftType;
      }
      else if (rightType.priority > leftType.priority) {
        cast.node = leftStrandsNode;
        cast.toType = rightType;
      }
      else {
        userError('type error', `A vector of length ${leftType.dimension} operated with a vector of length ${rightType.dimension} is not allowed.`);
      }

      const casted = primitiveConstructorNode(strandsContext, cast.toType, cast.node);

      if (cast.node === leftStrandsNode) {
        leftStrandsNode = createStrandsNode(casted.id, casted.dimension, strandsContext);
        finalLeftNodeID = leftStrandsNode.id;
      } else {
        rightStrandsNode = createStrandsNode(casted.id, casted.dimension, strandsContext);
        finalRightNodeID = rightStrandsNode.id;
      }
    }

    const nodeData = createNodeData({
      nodeType: NodeType.OPERATION,
      opCode,
      dependsOn: [finalLeftNodeID, finalRightNodeID],
      baseType: cast.toType.baseType,
      dimension: cast.toType.dimension,
    });
    const id = getOrCreateNode(dag, nodeData);
    recordInBasicBlock(cfg, cfg.currentBlock, id);
    return { id, dimension: nodeData.dimension };
  }

  function structInstanceNode(strandsContext, structTypeInfo, identifier, dependsOn) {
    const { cfg, dag } = strandsContext;
    if (dependsOn.length === 0) {
      for (const prop of structTypeInfo.properties) {
        const typeInfo = prop.dataType;
        const nodeData = createNodeData({
          nodeType: NodeType.VARIABLE,
          baseType: typeInfo.baseType,
          dimension: typeInfo.dimension,
          identifier: `${identifier}.${prop.name}`,
        });
        const componentID = getOrCreateNode(dag, nodeData);
        recordInBasicBlock(cfg, cfg.currentBlock, componentID);
        dependsOn.push(componentID);
      }
    }

    const nodeData = createNodeData({
      nodeType: NodeType.VARIABLE,
      dimension: structTypeInfo.properties.length,
      baseType: structTypeInfo.typeName,
      identifier,
      dependsOn
    });
    const structID = getOrCreateNode(dag, nodeData);
    recordInBasicBlock(cfg, cfg.currentBlock, structID);

    return { id: structID, dimension: 0, components: dependsOn };
  }

  function mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn) {
    const inputs = Array.isArray(dependsOn) ? dependsOn : [dependsOn];
    const mappedDependencies = [];
    let { dimension, baseType } = typeInfo;

    const dag = strandsContext.dag;
    let calculatedDimensions = 0;
    let originalNodeID = null;
    for (const dep of inputs.flat(Infinity)) {
      if (dep && dep.isStrandsNode) {
        const node = getNodeDataFromID(dag, dep.id);
        originalNodeID = dep.id;
        baseType = node.baseType;

        if (node.opCode === OpCode.Nary.CONSTRUCTOR) {
          for (const inner of node.dependsOn) {
            mappedDependencies.push(inner);
          }
        } else {
          mappedDependencies.push(dep.id);
        }

        calculatedDimensions += node.dimension;
        continue;
      }
      else if (typeof dep === 'number') {
        const { id, dimension } = scalarLiteralNode(strandsContext, { dimension: 1, baseType }, dep);
        mappedDependencies.push(id);
        calculatedDimensions += dimension;
        continue;
      }
      else {
        userError('type error', `You've tried to construct a scalar or vector type with a non-numeric value: ${dep}`);
      }
    }
    if (dimension === null) {
      dimension = calculatedDimensions;
    } else if (dimension > calculatedDimensions && calculatedDimensions === 1) {
      calculatedDimensions = dimension;
    } else if(calculatedDimensions !== 1 && calculatedDimensions !== dimension) {
      userError('type error', `You've tried to construct a ${baseType + dimension} with ${calculatedDimensions} components`);
    }
    const inferredTypeInfo = {
      dimension,
      baseType,
      priority: BasePriority[baseType],
    };
    return { originalNodeID, mappedDependencies, inferredTypeInfo };
  }

  function constructTypeFromIDs(strandsContext, typeInfo, strandsNodesArray) {
    const nodeData = createNodeData({
      nodeType: NodeType.OPERATION,
      opCode: OpCode.Nary.CONSTRUCTOR,
      dimension: typeInfo.dimension,
      baseType: typeInfo.baseType,
      dependsOn: strandsNodesArray
    });
    const id = getOrCreateNode(strandsContext.dag, nodeData);
    return id;
  }

  function primitiveConstructorNode(strandsContext, typeInfo, dependsOn) {
    const cfg = strandsContext.cfg;
    const { mappedDependencies, inferredTypeInfo } = mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn);

    const finalType = {
      baseType: typeInfo.baseType,
      dimension: inferredTypeInfo.dimension
    };

    const id = constructTypeFromIDs(strandsContext, finalType, mappedDependencies);
    if (typeInfo.baseType !== BaseType.DEFER) {
      recordInBasicBlock(cfg, cfg.currentBlock, id);
    }

    return { id, dimension: finalType.dimension, components: mappedDependencies };
  }

  function structConstructorNode(strandsContext, structTypeInfo, rawUserArgs) {
    const { cfg, dag } = strandsContext;
    const { properties } = structTypeInfo;

    if (!(rawUserArgs.length === properties.length)) {
      userError('type error',
        `You've tried to construct a ${structTypeInfo.typeName} struct with ${rawUserArgs.length} properties, but it expects ${properties.length} properties.\n` +
        `The properties it expects are:\n` +
        `${properties.map(prop => prop.name + ' ' + prop.DataType.baseType + prop.DataType.dimension)}`
      );
    }

    const dependsOn = [];
    for (let i = 0; i < properties.length; i++) {
      const expectedProperty = properties[i];
      const { originalNodeID, mappedDependencies } = mapPrimitiveDepsToIDs(strandsContext, expectedProperty.dataType, rawUserArgs[i]);
      if (originalNodeID) {
        dependsOn.push(originalNodeID);
      }
      else {
        dependsOn.push(
          constructTypeFromIDs(strandsContext, expectedProperty.dataType, mappedDependencies)
        );
      }
    }

    const nodeData = createNodeData({
      nodeType: NodeType.OPERATION,
      opCode: OpCode.Nary.CONSTRUCTOR,
      dimension: properties.length,
      baseType: structTypeInfo.typeName ,
      dependsOn
    });
    const id = getOrCreateNode(dag, nodeData);
    recordInBasicBlock(cfg, cfg.currentBlock, id);
    return { id, dimension: properties.length, components: structTypeInfo.components };
  }

  function functionCallNode(
    strandsContext,
    functionName,
    rawUserArgs,
    { overloads: rawOverloads } = {},
  ) {
    const { cfg, dag } = strandsContext;
    const overloads = rawOverloads || strandsBuiltinFunctions[functionName];

    const preprocessedArgs = rawUserArgs.map((rawUserArg) => mapPrimitiveDepsToIDs(strandsContext, DataType.defer, rawUserArg));
    const matchingArgsCounts = overloads.filter(overload => overload.params.length === preprocessedArgs.length);
    if (matchingArgsCounts.length === 0) {
      const argsLengthSet = new Set();
      const argsLengthArr = [];
      overloads.forEach((overload) => argsLengthSet.add(overload.params.length));
      argsLengthSet.forEach((len) => argsLengthArr.push(`${len}`));
      const argsLengthStr = argsLengthArr.join(', or ');
      userError("parameter validation error",`Function '${functionName}' has ${overloads.length} variants which expect ${argsLengthStr} arguments, but ${preprocessedArgs.length} arguments were provided.`);
    }

    const isGeneric = (T) => T.dimension === null;
    let bestOverload = null;
    let bestScore = 0;
    let inferredReturnType = null;
    let inferredDimension = null;

    for (const overload of matchingArgsCounts) {
      let isValid = true;
      let similarity = 0;

      for (let i = 0; i < preprocessedArgs.length; i++) {
        const preArg = preprocessedArgs[i];
        const argType = preArg.inferredTypeInfo;
        const expectedType = overload.params[i];
        let dimension = expectedType.dimension;

        if (isGeneric(expectedType)) {
          if (inferredDimension === null || inferredDimension === 1) {
            inferredDimension = argType.dimension;
          }

          if (inferredDimension !== argType.dimension &&
            !(argType.dimension === 1 && inferredDimension >= 1)
            ) {
            isValid = false;
          }
          dimension = inferredDimension;
        }
        else {
          if (argType.dimension > dimension) {
            isValid = false;
          }
        }

        if (argType.baseType === expectedType.baseType) {
          similarity += 2;
        }
        else if(expectedType.priority > argType.priority) {
          similarity += 1;
        }

      }

      if (isValid && (!bestOverload || similarity > bestScore)) {
        bestOverload = overload;
        bestScore = similarity;
        inferredReturnType =  {...overload.returnType };
        if (isGeneric(inferredReturnType)) {
          inferredReturnType.dimension = inferredDimension;
        }
      }
    }

    if (bestOverload === null) {
      userError('parameter validation', `No matching overload for ${functionName} was found!`);
    }

    let dependsOn = [];
    for (let i = 0; i < bestOverload.params.length; i++) {
      const arg = preprocessedArgs[i];
      const paramType = { ...bestOverload.params[i] };
      if (isGeneric(paramType)) {
        paramType.dimension = inferredDimension;
      }
      if (arg.originalNodeID && typeEquals(arg.inferredTypeInfo, paramType)) {
        dependsOn.push(arg.originalNodeID);
      }
      else {
        const castedArgID = constructTypeFromIDs(strandsContext, paramType, arg.mappedDependencies);
        recordInBasicBlock(cfg, cfg.currentBlock, castedArgID);
        dependsOn.push(castedArgID);
      }
    }

    const nodeData = createNodeData({
      nodeType: NodeType.OPERATION,
      opCode: OpCode.Nary.FUNCTION_CALL,
      identifier: functionName,
      dependsOn,
      baseType: inferredReturnType.baseType,
      dimension: inferredReturnType.dimension
    });
    const id = getOrCreateNode(dag, nodeData);
    recordInBasicBlock(cfg, cfg.currentBlock, id);
    return { id, dimension: inferredReturnType.dimension  };
  }

  function statementNode(strandsContext, statementType) {
    const { dag, cfg } = strandsContext;
    const nodeData = createNodeData({
      nodeType: NodeType.STATEMENT,
      statementType
    });
    const id = getOrCreateNode(dag, nodeData);
    recordInBasicBlock(cfg, cfg.currentBlock, id);
    return id;
  }

  function swizzleNode(strandsContext, parentNode, swizzle) {
    const { dag, cfg } = strandsContext;
    const baseType = dag.baseTypes[parentNode.id];
    const nodeData = createNodeData({
      nodeType: NodeType.OPERATION,
      baseType,
      dimension: swizzle.length,
      opCode: OpCode.Unary.SWIZZLE,
      dependsOn: [parentNode.id],
      swizzle,
    });
    const id = getOrCreateNode(dag, nodeData);
    recordInBasicBlock(cfg, cfg.currentBlock, id);
    return { id, dimension: swizzle.length };
  }

  function swizzleTrap(id, dimension, strandsContext, onRebind) {
      const swizzleSets = [
        ['x', 'y', 'z', 'w'],
        ['r', 'g', 'b', 'a'],
        ['s', 't', 'p', 'q']
      ].map(s => s.slice(0, dimension));
      const trap = {
        get(target, property, receiver) {
          if (property in target) {
            return Reflect.get(...arguments);
          } else {
            for (const set of swizzleSets) {
              if ([...property.toString()].every(char => set.includes(char))) {
                const swizzle = [...property].map(char => {
                  const index = set.indexOf(char);
                  return swizzleSets[0][index];
                }).join('');
                const node = swizzleNode(strandsContext, target, swizzle);
                return createStrandsNode(node.id, node.dimension, strandsContext);
              }
            }
          }
      },
    set(target, property, value, receiver) {
      for (const swizzleSet of swizzleSets) {
        const chars = [...property];
        const valid =
          chars.every(c => swizzleSet.includes(c)) &&
          new Set(chars).size === chars.length &&
          target.dimension >= chars.length;
        if (!valid) continue;

        const dim = target.dimension;

        // lanes are the underlying values of the target vector
        //  e.g. lane 0 holds the value aliased by 'x', 'r', and 's'
        // the lanes array is in the 'correct' order
        const lanes = new Array(dim);
        for (let i = 0; i < dim; i++) {
          const { id, dimension } = swizzleNode(strandsContext, target, 'xyzw'[i]);
          lanes[i] = createStrandsNode(id, dimension, strandsContext);
        }

        // The scalars array contains the individual components of the users values.
        // This may not be the most efficient way, as we swizzle each component individually,
        // so that .xyz becomes .x, .y, .z
        let scalars = [];
        if (value instanceof StrandsNode) {
          if (value.dimension === 1) {
            scalars = Array(chars.length).fill(value);
          } else if (value.dimension === chars.length) {
            for (let k = 0; k < chars.length; k++) {
              const { id, dimension } = swizzleNode(strandsContext, value, 'xyzw'[k]);
              scalars.push(createStrandsNode(id, dimension, strandsContext));
            }
          } else {
            userError('type error', `Swizzle assignment: RHS vector does not match LHS vector (need ${chars.length}, got ${value.dimension}).`);
          }
        } else if (Array.isArray(value)) {
          const flat = value.flat(Infinity);
          if (flat.length === 1) {
            scalars = Array(chars.length).fill(flat[0]);
          } else if (flat.length === chars.length) {
            scalars = flat;
          } else {
            userError('type error', `Swizzle assignment: RHS length ${flat.length} does not match ${chars.length}.`);
          }
        } else if (typeof value === 'number') {
          scalars = Array(chars.length).fill(value);
        } else {
          userError('type error', `Unsupported RHS for swizzle assignment: ${value}`);
        }

        // The canonical index refers to the actual value's position in the vector lanes
        // i.e. we are finding (3,2,1) from .zyx
        // We set the correct value in the lanes array
        for (let j = 0; j < chars.length; j++) {
          const canonicalIndex = swizzleSet.indexOf(chars[j]);
          lanes[canonicalIndex] = scalars[j];
        }

        const orig = getNodeDataFromID(strandsContext.dag, target.id);
        const baseType = orig?.baseType ?? BaseType.FLOAT;
        const { id: newID } = primitiveConstructorNode(
          strandsContext,
          { baseType, dimension: dim },
          lanes
        );

        target.id = newID;

        // If we swizzle assign on a struct component i.e.
        //   inputs.position.rg = [1, 2]
        // The onRebind callback will update the structs components so that it refers to the new values,
        // and make a new ID for the struct with these new values
        if (typeof onRebind === 'function') {
          onRebind(newID);
        }
        return true;
      }
      return Reflect.set(...arguments);
    }
    };
    return trap;
  }

  function shouldCreateTemp(dag, nodeID) {
    const nodeType = dag.nodeTypes[nodeID];
    if (nodeType !== NodeType.OPERATION) return false;
    if (dag.baseTypes[nodeID] === BaseType.SAMPLER2D) return false;
    const uses = dag.usedBy[nodeID] || [];
    return uses.length > 1;
  }
  const TypeNames = {
    'float1': 'float',
    'float2': 'vec2',
    'float3': 'vec3',
    'float4': 'vec4',
    'int1': 'int',
    'int2': 'ivec2',
    'int3': 'ivec3',
    'int4': 'ivec4',
    'bool1': 'bool',
    'bool2': 'bvec2',
    'bool3': 'bvec3',
    'bool4': 'bvec4',
    'mat2': 'mat2x2',
    'mat3': 'mat3x3',
    'mat4': 'mat4x4',
  };
  const cfgHandlers = {
    [BlockType.DEFAULT]: (blockID, strandsContext, generationContext) => {
      const { dag, cfg } = strandsContext;
      const instructions = cfg.blockInstructions[blockID] || [];
      for (const nodeID of instructions) {
        const nodeType = dag.nodeTypes[nodeID];
        if (shouldCreateTemp(dag, nodeID)) {
          const declaration = glslBackend.generateDeclaration(generationContext, dag, nodeID);
          generationContext.write(declaration);
        }
        if (nodeType === NodeType.STATEMENT) {
          glslBackend.generateStatement(generationContext, dag, nodeID);
        }
        if (nodeType === NodeType.ASSIGNMENT) {
          glslBackend.generateAssignment(generationContext, dag, nodeID);
          generationContext.visitedNodes.add(nodeID);
        }
      }
    },
    [BlockType.BRANCH](blockID, strandsContext, generationContext) {
      const { dag, cfg } = strandsContext;
      // Find all phi nodes in this branch block and declare them
      const blockInstructions = cfg.blockInstructions[blockID] || [];
      for (const nodeID of blockInstructions) {
        const node = getNodeDataFromID(dag, nodeID);
        if (node.nodeType === NodeType.PHI) {
          // Check if the phi node's first dependency already has a temp name
          const dependsOn = node.dependsOn || [];
          if (dependsOn.length > 0) {
            const firstDependency = dependsOn[0];
            const existingTempName = generationContext.tempNames[firstDependency];
            if (existingTempName) {
              // Reuse the existing temp name instead of creating a new one
              generationContext.tempNames[nodeID] = existingTempName;
              continue; // Skip declaration, just alias to existing variable
            }
          }

          // Otherwise, create a new temp variable for the phi node
          const tmp = `T${generationContext.nextTempID++}`;
          generationContext.tempNames[nodeID] = tmp;
          const T = extractNodeTypeInfo(dag, nodeID);
          const typeName = glslBackend.getTypeName(T.baseType, T.dimension);
          generationContext.write(`${typeName} ${tmp};`);
        }
      }
      this[BlockType.DEFAULT](blockID, strandsContext, generationContext);
    },
    [BlockType.IF_COND](blockID, strandsContext, generationContext) {
      const { dag, cfg } = strandsContext;
      const conditionID = cfg.blockConditions[blockID];
      const condExpr = glslBackend.generateExpression(generationContext, dag, conditionID);
      generationContext.write(`if (${condExpr})`);
      this[BlockType.DEFAULT](blockID, strandsContext, generationContext);
    },
    [BlockType.ELSE_COND](blockID, strandsContext, generationContext) {
      generationContext.write(`else`);
      this[BlockType.DEFAULT](blockID, strandsContext, generationContext);
    },
    [BlockType.IF_BODY](blockID, strandsContext, generationContext) {
      this[BlockType.DEFAULT](blockID, strandsContext, generationContext);
      this.assignPhiNodeValues(blockID, strandsContext, generationContext);
    },
    [BlockType.SCOPE_START](blockID, strandsContext, generationContext) {
      generationContext.write(`{`);
      generationContext.indent++;
    },
    [BlockType.SCOPE_END](blockID, strandsContext, generationContext) {
      generationContext.indent--;
      generationContext.write(`}`);
    },
    [BlockType.MERGE](blockID, strandsContext, generationContext) {
      this[BlockType.DEFAULT](blockID, strandsContext, generationContext);
    },
    [BlockType.FUNCTION](blockID, strandsContext, generationContext) {
      this[BlockType.DEFAULT](blockID, strandsContext, generationContext);
    },
    [BlockType.FOR](blockID, strandsContext, generationContext) {
      const { dag, cfg } = strandsContext;
      const instructions = cfg.blockInstructions[blockID] || [];

      generationContext.write(`for (`);

      // Set flag to suppress semicolon on the last statement
      const originalSuppressSemicolon = generationContext.suppressSemicolon;

      for (let i = 0; i < instructions.length; i++) {
        const nodeID = instructions[i];
        const node = getNodeDataFromID(dag, nodeID);
        const isLast = i === instructions.length - 1;

        // Suppress semicolon on the last statement
        generationContext.suppressSemicolon = isLast;

        if (shouldCreateTemp(dag, nodeID)) {
          const declaration = glslBackend.generateDeclaration(generationContext, dag, nodeID);
          generationContext.write(declaration);
        }
        if (node.nodeType === NodeType.STATEMENT) {
          glslBackend.generateStatement(generationContext, dag, nodeID);
        }
        if (node.nodeType === NodeType.ASSIGNMENT) {
          glslBackend.generateAssignment(generationContext, dag, nodeID);
          generationContext.visitedNodes.add(nodeID);
        }
      }

      // Restore original flag
      generationContext.suppressSemicolon = originalSuppressSemicolon;

      generationContext.write(`)`);
    },
    assignPhiNodeValues(blockID, strandsContext, generationContext) {
      const { dag, cfg } = strandsContext;
      // Find all phi nodes that this block feeds into
      const successors = cfg.outgoingEdges[blockID] || [];
      for (const successorBlockID of successors) {
        const instructions = cfg.blockInstructions[successorBlockID] || [];
        for (const nodeID of instructions) {
          const node = getNodeDataFromID(dag, nodeID);
          if (node.nodeType === NodeType.PHI) {
            // Find which input of this phi node corresponds to our block
            const branchIndex = node.phiBlocks?.indexOf(blockID);
            if (branchIndex !== -1 && branchIndex < node.dependsOn.length) {
              const sourceNodeID = node.dependsOn[branchIndex];
              const tempName = generationContext.tempNames[nodeID];
              if (tempName && sourceNodeID !== null) {
                const sourceExpr = glslBackend.generateExpression(generationContext, dag, sourceNodeID);
                generationContext.write(`${tempName} = ${sourceExpr};`);
              }
            }
          }
        }
      }
    },
  };
  const glslBackend = {
    hookEntry(hookType) {
      const firstLine = `(${hookType.parameters.flatMap((param) => {
      return `${param.qualifiers?.length ? param.qualifiers.join(' ') : ''}${param.type.typeName} ${param.name}`;
    }).join(', ')}) {`;
      return firstLine;
    },
    getTypeName(baseType, dimension) {
      const primitiveTypeName = TypeNames[baseType + dimension];
      if (!primitiveTypeName) {
        return baseType;
      }
      return primitiveTypeName;
    },
    generateHookUniformKey(name, typeInfo) {
      return `${this.getTypeName(typeInfo.baseType, typeInfo.dimension)} ${name}`;
    },
    generateVaryingVariable(varName, typeInfo) {
      return `${typeInfo.fnName} ${varName}`;
    },
    generateLocalDeclaration(varName, typeInfo) {
      const typeName = typeInfo.fnName;
      return `${typeName} ${varName};`;
    },
    generateStatement(generationContext, dag, nodeID) {
      const node = getNodeDataFromID(dag, nodeID);
      // Generate the expression followed by semicolon (unless suppressed)
      const semicolon = generationContext.suppressSemicolon ? '' : ';';
      if (node.statementType === StatementType.DISCARD) {
        generationContext.write(`discard${semicolon}`);
      } else if (node.statementType === StatementType.BREAK) {
        generationContext.write(`break${semicolon}`);
      } else if (node.statementType === StatementType.EXPRESSION) {
        const exprNodeID = node.dependsOn[0];
        const expr = this.generateExpression(generationContext, dag, exprNodeID);
        generationContext.write(`${expr}${semicolon}`);
      } else if (node.statementType === StatementType.EMPTY) {
        generationContext.write(semicolon);
      } else if (node.statementType === StatementType.EARLY_RETURN) {
        const exprNodeID = node.dependsOn[0];
        const expr = this.generateExpression(generationContext, dag, exprNodeID);
        generationContext.write(`return ${expr}${semicolon}`);
      }
    },
    generateAssignment(generationContext, dag, nodeID) {
      const node = getNodeDataFromID(dag, nodeID);
      // dependsOn[0] = targetNodeID, dependsOn[1] = sourceNodeID
      const targetNodeID = node.dependsOn[0];
      const sourceNodeID = node.dependsOn[1];

      // Generate the target expression (could be variable or swizzle)
      const targetExpr = this.generateExpression(generationContext, dag, targetNodeID);
      const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID);
      const semicolon = generationContext.suppressSemicolon ? '' : ';';

      // Generate assignment if we have both target and source
      if (targetExpr && sourceExpr && targetExpr !== sourceExpr) {
        generationContext.write(`${targetExpr} = ${sourceExpr}${semicolon}`);
      }
    },
    generateDeclaration(generationContext, dag, nodeID) {
      const expr = this.generateExpression(generationContext, dag, nodeID);
      const tmp = `T${generationContext.nextTempID++}`;
      generationContext.tempNames[nodeID] = tmp;
      const T = extractNodeTypeInfo(dag, nodeID);
      const typeName = this.getTypeName(T.baseType, T.dimension);
      return `${typeName} ${tmp} = ${expr};`;
    },
    generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType) {
      const dag = strandsContext.dag;
      const rootNode = getNodeDataFromID(dag, rootNodeID);
      if (isStructType(returnType)) {
        const structTypeInfo = returnType;
        for (let i = 0; i < structTypeInfo.properties.length; i++) {
          const prop = structTypeInfo.properties[i];
          const val = this.generateExpression(generationContext, dag, rootNode.dependsOn[i]);
          if (prop.name !== val) {
            generationContext.write(
              `${rootNode.identifier}.${prop.name} = ${val};`
            );
          }
        }
      }
      generationContext.write(`return ${this.generateExpression(generationContext, dag, rootNodeID)};`);
    },
    generateExpression(generationContext, dag, nodeID) {
      const node = getNodeDataFromID(dag, nodeID);
      if (generationContext.tempNames?.[nodeID]) {
        return generationContext.tempNames[nodeID];
      }
      switch (node.nodeType) {
        case NodeType.LITERAL:
        if (node.baseType === BaseType.FLOAT) {
          return node.value.toFixed(4);
        }
        else {
          return node.value;
        }
        case NodeType.VARIABLE:
        // Track shared variable usage context
        if (generationContext.shaderContext && generationContext.strandsContext?.sharedVariables?.has(node.identifier)) {
          const sharedVar = generationContext.strandsContext.sharedVariables.get(node.identifier);
          if (generationContext.shaderContext === 'vertex') {
            sharedVar.usedInVertex = true;
          } else if (generationContext.shaderContext === 'fragment') {
            sharedVar.usedInFragment = true;
          }
        }
        return node.identifier;
        case NodeType.OPERATION:
        const useParantheses = node.usedBy.length > 0;
        if (node.opCode === OpCode.Nary.CONSTRUCTOR) {
          // TODO: differentiate casts and constructors for more efficient codegen.
          // if (node.dependsOn.length === 1 && node.dimension === 1) {
          //   return this.generateExpression(generationContext, dag, node.dependsOn[0]);
          // }
          if (node.baseType === BaseType.SAMPLER2D) {
            return this.generateExpression(generationContext, dag, node.dependsOn[0]);
          }
          const T = this.getTypeName(node.baseType, node.dimension);
          const deps = node.dependsOn.map((dep) => this.generateExpression(generationContext, dag, dep));
          return `${T}(${deps.join(', ')})`;
        }
        if (node.opCode === OpCode.Nary.FUNCTION_CALL) {
          const functionArgs = node.dependsOn.map(arg =>this.generateExpression(generationContext, dag, arg));
          return `${node.identifier}(${functionArgs.join(', ')})`;
        }
        if (node.opCode === OpCode.Binary.MEMBER_ACCESS) {
          const [lID, rID] = node.dependsOn;
          const lName = this.generateExpression(generationContext, dag, lID);
          const rName = this.generateExpression(generationContext, dag, rID);
          return `${lName}.${rName}`;
        }
        if (node.opCode === OpCode.Unary.SWIZZLE) {
          const parentID = node.dependsOn[0];
          const parentExpr = this.generateExpression(generationContext, dag, parentID);
          return `${parentExpr}.${node.swizzle}`;
        }
        if (node.dependsOn.length === 2) {
          const [lID, rID] = node.dependsOn;
          const left  = this.generateExpression(generationContext, dag, lID);
          const right = this.generateExpression(generationContext, dag, rID);

          // Special case for modulo: use mod() function for floats in GLSL
          if (node.opCode === OpCode.Binary.MODULO) {
            const leftNode = getNodeDataFromID(dag, lID);
            const rightNode = getNodeDataFromID(dag, rID);
            // If either operand is float, use mod() function
            if (leftNode.baseType === BaseType.FLOAT || rightNode.baseType === BaseType.FLOAT) {
              return `mod(${left}, ${right})`;
            }
            // For integers, use % operator
            return `(${left} % ${right})`;
          }

          const opSym = OpCodeToSymbol[node.opCode];
          if (useParantheses) {
            return `(${left} ${opSym} ${right})`;
          } else {
            return `${left} ${opSym} ${right}`;
          }
        }
        if (node.opCode === OpCode.Unary.LOGICAL_NOT
          || node.opCode === OpCode.Unary.NEGATE
          || node.opCode === OpCode.Unary.PLUS
          ) {
          const [i] = node.dependsOn;
          const val  = this.generateExpression(generationContext, dag, i);
          const sym  = OpCodeToSymbol[node.opCode];
          return `${sym}${val}`;
        }
        case NodeType.PHI:
        // Phi nodes represent conditional merging of values
        // If this phi node has an identifier (like varying variables), use that
        if (node.identifier) {
          return node.identifier;
        }
        // Otherwise, they should have been declared as temporary variables
        // and assigned in the appropriate branches
        if (generationContext.tempNames?.[nodeID]) {
          return generationContext.tempNames[nodeID];
        } else {
          // If no temp was created, this phi node only has one input
          // so we can just use that directly
          const validInputs = node.dependsOn.filter(id => id !== null);
          if (validInputs.length > 0) {
            return this.generateExpression(generationContext, dag, validInputs[0]);
          } else {
            throw new Error(`No valid inputs for node`)
          }
        }
        case NodeType.ASSIGNMENT:
        internalError(`ASSIGNMENT nodes should not be used as expressions`);
        default:
        internalError(`${NodeTypeToName[node.nodeType]} code generation not implemented yet`);
      }
    },
    generateBlock(blockID, strandsContext, generationContext) {
      const type = strandsContext.cfg.blockTypes[blockID];
      const handler = cfgHandlers[type] || cfgHandlers[BlockType.DEFAULT];
      handler.call(cfgHandlers, blockID, strandsContext, generationContext);
    },

    createGetTextureCall(strandsContext, args) {
      // In GLSL, getTexture is straightforward - just pass through the args
      // First argument should be a texture (sampler2D), second should be coordinates
      const { id, dimension } = functionCallNode(strandsContext, 'getTexture', args, {
        overloads: [{
          params: [DataType.sampler2D, DataType.float2],
          returnType: DataType.float4
        }]
      });
      return { id, dimension };
    },

    instanceIdReference() {
      return 'gl_InstanceID';
    },
  };

  /*
   * Shared utility function for parsing shader hook types from GLSL shader source
   */
  function getShaderHookTypes(shader, hookName) {
    let fullSrc = shader._vertSrc;
    let body = shader.hooks.vertex[hookName];
    if (!body) {
      body = shader.hooks.fragment[hookName];
      fullSrc = shader._fragSrc;
    }
    if (!body) {
      throw new Error(`Can't find hook ${hookName}!`);
    }
    const nameParts = hookName.split(/\s+/g);
    const functionName = nameParts.pop();
    const returnType = nameParts.pop();
    const returnQualifiers = [...nameParts];
    const parameterMatch = /\(([^\)]*)\)/.exec(body);
    if (!parameterMatch) {
      throw new Error(`Couldn't find function parameters in hook body:\n${body}`);
    }
    const structProperties = structName => {
      const structDefMatch = new RegExp(`struct\\s+${structName}\\s*{([^}]*)}`).exec(fullSrc);
      if (!structDefMatch) return undefined;
      const properties = [];
      for (const defSrc of structDefMatch[1].split(';')) {
        // E.g. `int var1, var2;` or `MyStruct prop;`
        const parts = defSrc.trim().split(/\s+|,/g);
        const typeName = parts.shift();
        const names = [...parts];
        const typeProperties = structProperties(typeName);
        for (const name of names) {
          const dataType = TypeInfoFromGLSLName[typeName] || null;
          properties.push({
            name,
            type: {
              typeName,
              qualifiers: [],
              properties: typeProperties,
              dataType,
            }
          });
        }
      }
      return properties;
    };
    const parameters = parameterMatch[1].split(',').map(paramString => {
      // e.g. `int prop` or `in sampler2D prop` or `const float prop`
      const parts = paramString.trim().split(/\s+/g);
      const name = parts.pop();
      const typeName = parts.pop();
      const qualifiers = [...parts];
      const properties = structProperties(typeName);
      const dataType = TypeInfoFromGLSLName[typeName] || null;
      return {
        name,
        type: {
          typeName,
          qualifiers,
          properties,
          dataType,
        }
      };
    });
    const dataType = TypeInfoFromGLSLName[returnType] || null;
    return {
      name: functionName,
      returnType: {
        typeName: returnType,
        qualifiers: returnQualifiers,
        properties: structProperties(returnType),
        dataType,
      },
      parameters
    };
  }

  var noiseGLSL = "// Based on https://github.com/stegu/webgl-noise/blob/22434e04d7753f7e949e8d724ab3da2864c17a0f/src/noise3D.glsl\n// MIT licensed, adapted for p5.strands\n\nvec3 mod289(vec3 x) {\n  return x - floor(x * (1.0 / 289.0)) * 289.0;\n}\n\nvec4 mod289(vec4 x) {\n  return x - floor(x * (1.0 / 289.0)) * 289.0;\n}\n\nvec4 permute(vec4 x) {\n  return mod289(((x*34.0)+10.0)*x);\n}\n\nvec4 taylorInvSqrt(vec4 r)\n{\n  return 1.79284291400159 - 0.85373472095314 * r;\n}\n\nfloat baseNoise(vec3 v)\n{\n  const vec2  C = vec2(1.0/6.0, 1.0/3.0) ;\n  const vec4  D = vec4(0.0, 0.5, 1.0, 2.0);\n\n  // First corner\n  vec3 i  = floor(v + dot(v, C.yyy) );\n  vec3 x0 =   v - i + dot(i, C.xxx) ;\n\n  // Other corners\n  vec3 g = step(x0.yzx, x0.xyz);\n  vec3 l = 1.0 - g;\n  vec3 i1 = min( g.xyz, l.zxy );\n  vec3 i2 = max( g.xyz, l.zxy );\n\n  //   x0 = x0 - 0.0 + 0.0 * C.xxx;\n  //   x1 = x0 - i1  + 1.0 * C.xxx;\n  //   x2 = x0 - i2  + 2.0 * C.xxx;\n  //   x3 = x0 - 1.0 + 3.0 * C.xxx;\n  vec3 x1 = x0 - i1 + C.xxx;\n  vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y\n  vec3 x3 = x0 - D.yyy;      // -1.0+3.0*C.x = -0.5 = -D.y\n\n  // Permutations\n  i = mod289(i);\n  vec4 p = permute( permute( permute(\n          i.z + vec4(0.0, i1.z, i2.z, 1.0 ))\n        + i.y + vec4(0.0, i1.y, i2.y, 1.0 ))\n      + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));\n\n  // Gradients: 7x7 points over a square, mapped onto an octahedron.\n  // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)\n  float n_ = 0.142857142857; // 1.0/7.0\n  vec3  ns = n_ * D.wyz - D.xzx;\n\n  vec4 j = p - 49.0 * floor(p * ns.z * ns.z);  //  mod(p,7*7)\n\n  vec4 x_ = floor(j * ns.z);\n  vec4 y_ = floor(j - 7.0 * x_ );    // mod(j,N)\n\n  vec4 x = x_ *ns.x + ns.yyyy;\n  vec4 y = y_ *ns.x + ns.yyyy;\n  vec4 h = 1.0 - abs(x) - abs(y);\n\n  vec4 b0 = vec4( x.xy, y.xy );\n  vec4 b1 = vec4( x.zw, y.zw );\n\n  //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;\n  //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;\n  vec4 s0 = floor(b0)*2.0 + 1.0;\n  vec4 s1 = floor(b1)*2.0 + 1.0;\n  vec4 sh = -step(h, vec4(0.0));\n\n  vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;\n  vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;\n\n  vec3 p0 = vec3(a0.xy,h.x);\n  vec3 p1 = vec3(a0.zw,h.y);\n  vec3 p2 = vec3(a1.xy,h.z);\n  vec3 p3 = vec3(a1.zw,h.w);\n\n  //Normalise gradients\n  vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));\n  p0 *= norm.x;\n  p1 *= norm.y;\n  p2 *= norm.z;\n  p3 *= norm.w;\n\n  // Mix final noise value\n  vec4 m = max(0.5 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);\n  m = m * m;\n  return 105.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),\n        dot(p2,x2), dot(p3,x3) ) );\n}\n\nfloat noise(vec3 st, int octaves, float ampFalloff) {\n  float result = 0.0;\n  float amplitude = 1.0;\n  float frequency = 1.0;\n\n  for (int i = 0; i < 8; i++) {\n    if (i >= octaves) break;\n    result += amplitude * baseNoise(st * frequency);\n    frequency *= 2.0;\n    amplitude *= ampFalloff;\n  }\n  return (result + 1.0) * 0.5;\n}\n";

  class FilterRenderer2D {
    /**
     * Creates a new FilterRenderer2D instance.
     * @param {p5} parentRenderer - The p5.js instance.
     */
    constructor(parentRenderer) {
      this.parentRenderer = parentRenderer;
      // Create a canvas for applying WebGL-based filters
      this.canvas = document.createElement('canvas');
      this.canvas.width = parentRenderer.width;
      this.canvas.height = parentRenderer.height;

      // Initialize the WebGL context
      let webglVersion = WEBGL2;
      this.gl = this.canvas.getContext('webgl2');
      if (!this.gl) {
        webglVersion = WEBGL;
        this.gl = this.canvas.getContext('webgl');
      }
      if (!this.gl) {
        console.error('WebGL not supported, cannot apply filter.');
        return;
      }

      this.textures = new Map();

      // Minimal renderer object required by p5.Shader and p5.Texture
      this._renderer = {
        GL: this.gl,
        registerEnabled: new Set(),
        _curShader: null,
        _emptyTexture: null,
        webglVersion,
        states: {
          textureWrapX: CLAMP,
          textureWrapY: CLAMP,
        },
        _arraysEqual: (a, b) => JSON.stringify(a) === JSON.stringify(b),
        _getEmptyTexture: () => {
          if (!this._emptyTexture) {
            const im = new Image(1, 1);
            im.set(0, 0, 255);
            this._emptyTexture = new Texture(this._renderer, im);
          }
          return this._emptyTexture;
        },
        _initShader: (shader) => {
          const gl = this.gl;

          const vertShader = gl.createShader(gl.VERTEX_SHADER);
          gl.shaderSource(vertShader, shader.vertSrc());
          gl.compileShader(vertShader);
          if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) {
            throw new Error(`Yikes! An error occurred compiling the vertex shader: ${
            gl.getShaderInfoLog(vertShader)
          }`);
          }

          const fragShader = gl.createShader(gl.FRAGMENT_SHADER);
          gl.shaderSource(fragShader, shader.fragSrc());
          gl.compileShader(fragShader);
          if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS)) {
            throw new Error(`Darn! An error occurred compiling the fragment shader: ${
            gl.getShaderInfoLog(fragShader)
          }`);
          }

          const program = gl.createProgram();
          gl.attachShader(program, vertShader);
          gl.attachShader(program, fragShader);
          gl.linkProgram(program);

          if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
            throw new Error(
              `Snap! Error linking shader program: ${gl.getProgramInfoLog(program)}`
            );
          }

          shader._glProgram = program;
          shader._vertShader = vertShader;
          shader._fragShader = fragShader;
        },
        getTexture: (input) => {
          let src = input;
          if (src instanceof Framebuffer) {
            src = src.color;
          }

          const texture = this.textures.get(src);
          if (texture) {
            return texture;
          }

          const tex = new Texture(this._renderer, src);
          this.textures.set(src, tex);
          return tex;
        },
        populateHooks: (shader, src, shaderType) => {
          return populateGLSLHooks(shader, src, shaderType);
        },
        _getShaderAttributes: (shader) => {
          return getWebGLShaderAttributes(shader, this.gl);
        },
        getUniformMetadata: (shader) => {
          return getWebGLUniformMetadata(shader, this.gl);
        },
        _finalizeShader: () => {},
        _useShader: (shader) => {
          this.gl.useProgram(shader._glProgram);
        },
        bindTexture: (tex) => {
          // bind texture using gl context + glTarget and
          // generated gl texture object
          this.gl.bindTexture(this.gl.TEXTURE_2D, tex.getTexture().texture);
        },
        unbindTexture: () => {
          // unbind per above, disable texturing on glTarget
          this.gl.bindTexture(this.gl.TEXTURE_2D, null);
        },
        _unbindFramebufferTexture: (uniform) => {
          // Make sure an empty texture is bound to the slot so that we don't
          // accidentally leave a framebuffer bound, causing a feedback loop
          // when something else tries to write to it
          const gl = this.gl;
          const empty = this._getEmptyTexture();
          gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex);
          empty.bindTexture();
          gl.uniform1i(uniform.location, uniform.samplerIndex);
        },
        createTexture: ({ width, height, format, dataType }) => {
          const gl = this.gl;
          const tex = gl.createTexture();
          gl.bindTexture(gl.TEXTURE_2D, tex);
          gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0,
                             gl.RGBA, gl.UNSIGNED_BYTE, null);
          // TODO use format and data type
          return { texture: tex, glFormat: gl.RGBA, glDataType: gl.UNSIGNED_BYTE };
        },
        uploadTextureFromSource: ({ texture, glFormat, glDataType }, source) => {
          const gl = this.gl;
          gl.bindTexture(gl.TEXTURE_2D, texture);
          gl.texImage2D(gl.TEXTURE_2D, 0, glFormat, glFormat, glDataType, source);
        },
        uploadTextureFromData: ({ texture, glFormat, glDataType }, data, width, height) => {
          const gl = this.gl;
          gl.bindTexture(gl.TEXTURE_2D, texture);
          gl.texImage2D(
            gl.TEXTURE_2D,
            0,
            glFormat,
            width,
            height,
            0,
            glFormat,
            glDataType,
            data
          );
        },
        setTextureParams: (texture) => {
          return setWebGLTextureParams(texture, this.gl, this._renderer.webglVersion);
        },
        updateUniformValue: (shader, uniform, data) => {
          return setWebGLUniformValue(
            shader,
            uniform,
            data,
            (tex) => this._renderer.getTexture(tex),
            this.gl
          );
        },
        _enableAttrib: (_shader, attr, size, type, normalized, stride, offset) => {
          const loc = attr.location;
          const gl = this.gl;
          // Enable register even if it is disabled
          if (!this._renderer.registerEnabled.has(loc)) {
            gl.enableVertexAttribArray(loc);
            // Record register availability
            this._renderer.registerEnabled.add(loc);
          }
          gl.vertexAttribPointer(
            loc,
            size,
            type || gl.FLOAT,
            normalized || false,
            stride || 0,
            offset || 0
          );
        },
        _disableRemainingAttributes: (shader) => {
          for (const location of this._renderer.registerEnabled.values()) {
            if (
              !Object.keys(shader.attributes).some(
                key => shader.attributes[key].location === location
              )
            ) {
              this.gl.disableVertexAttribArray(location);
              this._renderer.registerEnabled.delete(location);
            }
          }
        },
        _updateTexture: (uniform, tex) => {
          const gl = this.gl;
          gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex);
          tex.bindTexture();
          tex.update();
          gl.uniform1i(uniform.location, uniform.samplerIndex);
        },
        baseFilterShader: () => this.baseFilterShader(),
        strandsBackend: glslBackend,
        getShaderHookTypes: (shader, hookName) => getShaderHookTypes(shader, hookName),
        uniformNameFromHookKey: (key) => key.slice(key.indexOf(' ') + 1),
      };

      this._baseFilterShader = undefined;

      // Store initialized shaders for each operation
      this.filterShaders = {};

      // These will be set by setOperation
      this.operation = null;
      this.filterParameter = 1;
      this.customShader = null;
      this._shader = null;

      // Create buffers once
      this.vertexBuffer = this.gl.createBuffer();
      this.texcoordBuffer = this.gl.createBuffer();

      // Set up the vertices and texture coordinates for a full-screen quad
      this.vertices = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
      this.texcoords = new Float32Array([0, 1, 1, 1, 0, 0, 1, 0]);

      // Upload vertex data once
      this._bindBufferData(
        this.vertexBuffer,
        this.gl.ARRAY_BUFFER,
        this.vertices
      );

      // Upload texcoord data once
      this._bindBufferData(
        this.texcoordBuffer,
        this.gl.ARRAY_BUFFER,
        this.texcoords
      );
    }

    _webGL2CompatibilityPrefix(shaderType, floatPrecision) {
      let code = '';
      if (this._renderer.webglVersion === WEBGL2) {
        code += '#version 300 es\n#define WEBGL2\n';
      }
      if (shaderType === 'vert') {
        code += '#define VERTEX_SHADER\n';
      } else if (shaderType === 'frag') {
        code += '#define FRAGMENT_SHADER\n';
      }
      if (floatPrecision) {
        code += `precision ${floatPrecision} float;\n`;
      }
      return code;
    }

    baseFilterShader() {
      if (!this._baseFilterShader) {
        this._baseFilterShader = new Shader(
          this._renderer,
          this._webGL2CompatibilityPrefix('vert', 'highp') +
            webgl2CompatibilityShader +
            filterBaseVert,
          this._webGL2CompatibilityPrefix('frag', 'highp') +
            webgl2CompatibilityShader +
            filterBaseFrag,
          {
            vertex: {},
            fragment: {
              'vec4 getColor': `(FilterInputs inputs, in sampler2D canvasContent) {
              return getTexture(canvasContent, inputs.texCoord);
            }`
            }
          }
        );
      }
      return this._baseFilterShader;
    }

    getNoiseShaderSnippet() {
      return noiseGLSL;
    }

    /**
     * Set the current filter operation and parameter. If a customShader is provided,
     * that overrides the operation-based shader.
     * @param {String} operation - The filter operation type (e.g., constants.BLUR).
     * @param {Number} filterParameter - The strength of the filter.
     * @param {p5.Shader} customShader - Optional custom shader.
     */
    setOperation(operation, filterParameter, customShader = null) {
      this.operation = operation;
      this.filterParameter = filterParameter;

      let useDefaultParam = operation in filterParamDefaults &&
        filterParameter === undefined;
      if (useDefaultParam) {
        this.filterParameter = filterParamDefaults[operation];
      }

      this.customShader = customShader;
      this._initializeShader();
    }

    /**
     * Initializes or retrieves the shader program for the current operation.
     * If a customShader is provided, that is used.
     * Otherwise, returns a cached shader if available, or creates a new one, caches it, and sets it as current.
     */
    _initializeShader() {
      if (this.customShader) {
        this._shader = this.customShader;
        return;
      }

      if (!this.operation) {
        console.error('No operation set for FilterRenderer2D, cannot initialize shader.');
        return;
      }

      // If we already have a compiled shader for this operation, reuse it
      if (this.filterShaders[this.operation]) {
        this._shader = this.filterShaders[this.operation];
        return;
      }

      // Use the shared makeFilterShader function from filterShaders.js
      const newShader = makeFilterShader(this._renderer, this.operation, this.parentRenderer._pInst);
      this.filterShaders[this.operation] = newShader;
      this._shader = newShader;
    }

    /**
     * Binds a buffer to the drawing context
     * when passed more than two arguments it also updates or initializes
     * the data associated with the buffer
     */
    _bindBufferData(buffer, target, values) {
      const gl = this.gl;
      gl.bindBuffer(target, buffer);
      gl.bufferData(target, values, gl.STATIC_DRAW);
    }

    get canvasTexture() {
      if (!this._canvasTexture) {
        this._canvasTexture = new Texture(this._renderer, this.parentRenderer.wrappedElt);
      }
      return this._canvasTexture;
    }

    /**
     * Prepares and runs the full-screen quad draw call.
     */
    _renderPass() {
      const gl = this.gl;
      this._shader.bindShader('fill');
      const pixelDensity = this.parentRenderer.pixelDensity ?
        this.parentRenderer.pixelDensity() : 1;

      const texelSize = [
        1 / (this.parentRenderer.width * pixelDensity),
        1 / (this.parentRenderer.height * pixelDensity)
      ];

      const canvasTexture = this.canvasTexture;

      // Set uniforms for the shader
      this._shader.setUniform('tex0', canvasTexture);
      this._shader.setUniform('texelSize', texelSize);
      this._shader.setUniform('canvasSize', [this.parentRenderer.width, this.parentRenderer.height]);
      this._shader.setUniform('radius', Math.max(1, this.filterParameter));
      this._shader.setUniform('filterParameter', this.filterParameter);
      this._shader.setDefaultUniforms();

      this.parentRenderer.states.setValue('rectMode', CORNER);
      this.parentRenderer.states.setValue('imageMode', CORNER);
      this.parentRenderer.blendMode(BLEND);
      this.parentRenderer.resetMatrix();


      const identityMatrix = [1, 0, 0, 0,
                              0, 1, 0, 0,
                              0, 0, 1, 0,
                              0, 0, 0, 1];
      this._shader.setUniform('uModelViewMatrix', identityMatrix);
      this._shader.setUniform('uProjectionMatrix', identityMatrix);

      // Bind and enable vertex attributes
      gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
      this._shader.enableAttrib(this._shader.attributes.aPosition, 2);

      gl.bindBuffer(gl.ARRAY_BUFFER, this.texcoordBuffer);
      this._shader.enableAttrib(this._shader.attributes.aTexCoord, 2);

      this._shader.bindTextures();
      this._renderer._disableRemainingAttributes(this._shader);

      // Draw the quad
      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
      // Unbind the shader
      this._shader.unbindShader();
    }

    /**
     * Applies the current filter operation. If the filter requires multiple passes (e.g. blur),
     * it handles those internally. Make sure setOperation() has been called before applyFilter().
     */
    applyFilter() {
      if (!this._shader) {
        console.error('Cannot apply filter: shader not initialized.');
        return;
      }
      this.parentRenderer.push();
      this.parentRenderer.resetMatrix();
      // For blur, we typically do two passes: one horizontal, one vertical.
      if (this.operation === BLUR && !this.customShader) {
        // Horizontal pass
        this._shader.setUniform('direction', [1, 0]);
        this._renderPass();

        // Draw the result onto itself
        this.parentRenderer.clear();
        this.parentRenderer.drawingContext.drawImage(
          this.canvas,
          0, 0,
          this.parentRenderer.width, this.parentRenderer.height
        );

        // Vertical pass
        this._shader.setUniform('direction', [0, 1]);
        this._renderPass();

        this.parentRenderer.clear();
        this.parentRenderer.drawingContext.drawImage(
          this.canvas,
          0, 0,
          this.parentRenderer.width, this.parentRenderer.height
        );
      } else {
        // Single-pass filters

        this._renderPass();
        this.parentRenderer.clear();
        // con
        this.parentRenderer.blendMode(BLEND);


        this.parentRenderer.drawingContext.drawImage(
          this.canvas,
          0, 0,
          this.parentRenderer.width, this.parentRenderer.height
        );
      }
      this.parentRenderer.pop();
    }
  }

  const styleEmpty = 'rgba(0,0,0,0)';
  // const alphaThreshold = 0.00125; // minimum visible

  class Renderer2D extends Renderer {
    constructor(pInst, w, h, isMainCanvas, elt, attributes = {}) {
      super(pInst, w, h, isMainCanvas);

      this.canvas = this.elt = elt || document.createElement('canvas');

      if (isMainCanvas) {
        // for pixel method sharing with pimage
        this._pInst._curElement = this;
        this._pInst.canvas = this.canvas;
      } else {
        // hide if offscreen buffer by default
        this.canvas.style.display = 'none';
      }

      this.elt.id = 'defaultCanvas0';
      this.elt.classList.add('p5Canvas');

      // Extend renderer with methods of p5.Element with getters
      for (const p of Object.getOwnPropertyNames(Element.prototype)) {
        if (p !== 'constructor' && p[0] !== '_') {
          Object.defineProperty(this, p, {
            get() {
              return this.wrappedElt[p];
            }
          });
        }
      }

      // Set canvas size
      this.elt.width = w * this._pixelDensity;
      this.elt.height = h * this._pixelDensity;
      this.elt.style.width = `${w}px`;
      this.elt.style.height = `${h}px`;

      // Attach canvas element to DOM
      if (this._pInst._userNode) {
        // user input node case
        this._pInst._userNode.appendChild(this.elt);
      } else {
        //create main element
        if (document.getElementsByTagName('main').length === 0) {
          let m = document.createElement('main');
          document.body.appendChild(m);
        }
        //append canvas to main
        document.getElementsByTagName('main')[0].appendChild(this.elt);
      }

      // Get and store drawing context
      this.drawingContext = this.canvas.getContext('2d', attributes);
      if(attributes.colorSpace === 'display-p3'){
        this.states.colorMode = RGBHDR;
      }
      this.scale(this._pixelDensity, this._pixelDensity);

      // Set and return p5.Element
      this.wrappedElt = new Element(this.elt, this._pInst);
      this.clipPath = null;
    }

    get filterRenderer() {
      if (!this._filterRenderer) {
        this._filterRenderer = new FilterRenderer2D(this);
      }
      return this._filterRenderer;
    }

    remove(){
      this.wrappedElt.remove();
      this.wrappedElt = null;
      this.canvas = null;
      this.elt = null;
    }

    getFilterGraphicsLayer() {
      // create hidden webgl renderer if it doesn't exist
      if (!this.filterGraphicsLayer) {
        const pInst = this._pInst;

        // create secondary layer
        this.filterGraphicsLayer =
          new Graphics(
            this.width,
            this.height,
            WEBGL,
            pInst
          );
      }
      if (
        this.filterGraphicsLayer.width !== this.width ||
        this.filterGraphicsLayer.height !== this.height
      ) {
        // Resize the graphics layer
        this.filterGraphicsLayer.resizeCanvas(this.width, this.height);
      }
      if (
        this.filterGraphicsLayer.pixelDensity() !== this._pInst.pixelDensity()
      ) {
        this.filterGraphicsLayer.pixelDensity(this._pInst.pixelDensity());
      }

      return this.filterGraphicsLayer;
    }

    _applyDefaults() {
      this.states.setValue('_cachedFillStyle', undefined);
      this.states.setValue('_cachedStrokeStyle', undefined);
      this._cachedBlendMode = BLEND;
      this._setFill(_DEFAULT_FILL);
      this._setStroke(_DEFAULT_STROKE);
      this.drawingContext.lineCap = ROUND;
      this.drawingContext.font = 'normal 12px sans-serif';
    }

    resize(w, h) {
      super.resize(w, h);

      // save canvas properties
      const props = {};
      for (const key in this.drawingContext) {
        const val = this.drawingContext[key];
        if (typeof val !== 'object' && typeof val !== 'function') {
          props[key] = val;
        }
      }

      this.canvas.width = w * this._pixelDensity;
      this.canvas.height = h * this._pixelDensity;
      this.canvas.style.width = `${w}px`;
      this.canvas.style.height = `${h}px`;
      this.drawingContext.scale(
        this._pixelDensity,
        this._pixelDensity
      );

      // reset canvas properties
      for (const savedKey in props) {
        try {
          this.drawingContext[savedKey] = props[savedKey];
        } catch (err) {
          // ignore read-only property errors
        }
      }
    }

    //////////////////////////////////////////////
    // COLOR | Setting
    //////////////////////////////////////////////

    background(...args) {
      this.push();
      this.resetMatrix();

      if (args[0] instanceof Image) {
        if (args[1] >= 0) {
          // set transparency of background
          const img = args[0];
          this.drawingContext.globalAlpha = args[1] / 255;
          this._pInst.image(img, 0, 0, this.width, this.height);
        } else {
          this._pInst.image(args[0], 0, 0, this.width, this.height);
        }
      } else {
        // create background rect
        const color = this._pInst.color(...args);

        // Add accessible outputs if the method exists; on success,
        // set the accessible output background to white.
        if (this._pInst._addAccsOutput?.()) {
          this._pInst._accsBackground?.(color._getRGBA([255, 255, 255, 255]));
        }

        const newFill = color.toString();
        this._setFill(newFill);

        if (this._isErasing) {
          this.blendMode(this._cachedBlendMode);
        }

        this.drawingContext.fillRect(0, 0, this.width, this.height);

        if (this._isErasing) {
          this._pInst.erase();
        }
      }
      this.pop();
    }

    clear() {
      this.drawingContext.save();
      this.resetMatrix();
      this.drawingContext.clearRect(0, 0, this.width, this.height);
      this.drawingContext.restore();
    }

    fill(...args) {
      super.fill(...args);
      const color = this.states.fillColor;
      this._setFill(color.toString());

      // Add accessible outputs if the method exists; on success,
      // set the accessible output background to white.
      if (this._pInst._addAccsOutput?.()) {
        this._pInst._accsCanvasColors?.('fill', color._getRGBA([255, 255, 255, 255]));
      }
    }

    stroke(...args) {
      super.stroke(...args);
      const color = this.states.strokeColor;
      this._setStroke(color.toString());

      // Add accessible outputs if the method exists; on success,
      // set the accessible output background to white.
      if (this._pInst._addAccsOutput?.()) {
        this._pInst._accsCanvasColors?.('stroke', color._getRGBA([255, 255, 255, 255]));
      }
    }

    erase(opacityFill, opacityStroke) {
      if (!this._isErasing) {
        // cache the fill style
        this.states.setValue('_cachedFillStyle', this.drawingContext.fillStyle);
        const newFill = this._pInst.color(255, opacityFill).toString();
        this.drawingContext.fillStyle = newFill;

        // cache the stroke style
        this.states.setValue('_cachedStrokeStyle', this.drawingContext.strokeStyle);
        const newStroke = this._pInst.color(255, opacityStroke).toString();
        this.drawingContext.strokeStyle = newStroke;

        // cache blendMode
        const tempBlendMode = this._cachedBlendMode;
        this.blendMode(REMOVE);
        this._cachedBlendMode = tempBlendMode;

        this._isErasing = true;
      }
    }

    noErase() {
      if (this._isErasing) {
        this.drawingContext.fillStyle = this.states._cachedFillStyle;
        this.drawingContext.strokeStyle = this.states._cachedStrokeStyle;

        this.blendMode(this._cachedBlendMode);
        this._isErasing = false;
      }
    }

    drawShape(shape) {
      const visitor = new PrimitiveToPath2DConverter({
        strokeWeight: this.states.strokeWeight
      });
      shape.accept(visitor);
      if (this._clipping) {
        const currentTransform = this.drawingContext.getTransform();
        const clipBaseTransform = this._clipBaseTransform.inverse();
        const relativeTransform = clipBaseTransform.multiply(currentTransform);
        this.clipPath.addPath(visitor.path, relativeTransform);
        this.clipPath.closePath();
      } else {
        if (this.states.fillColor) {
          this.drawingContext.fill(visitor.path);
        }
        if (this.states.strokeColor) {
          this.drawingContext.stroke(visitor.path);
        }
      }
    }

    beginClip(options = {}) {
      super.beginClip(options);
      this._clipBaseTransform = this.drawingContext.getTransform();
      // cache the fill style
      this.states.setValue('_cachedFillStyle', this.drawingContext.fillStyle);
      const newFill = this._pInst.color(255, 0).toString();
      this.drawingContext.fillStyle = newFill;

      // cache the stroke style
      this.states.setValue('_cachedStrokeStyle', this.drawingContext.strokeStyle);
      const newStroke = this._pInst.color(255, 0).toString();
      this.drawingContext.strokeStyle = newStroke;

      // cache blendMode
      const tempBlendMode = this._cachedBlendMode;
      this.blendMode(BLEND);
      this._cachedBlendMode = tempBlendMode;

      // Since everything must be in one path, create a new single Path2D to chain all shapes onto.
      // Start a new path. Everything from here on out should become part of this
      // one path so that we can clip to the whole thing.
      this.clipPath = new Path2D();
      this._clipBaseTransform = this.drawingContext.getTransform();

      if (this._clipInvert) {
        // Slight hack: draw a big rectangle over everything with reverse winding
        // order. This is hopefully large enough to cover most things.
        this.clipPath.moveTo(
          -2 * this.width,
          -2 * this.height
        );
        this.clipPath.lineTo(
          -2 * this.width,
          2 * this.height
        );
        this.clipPath.lineTo(
          2 * this.width,
          2 * this.height
        );
        this.clipPath.lineTo(
          2 * this.width,
          -2 * this.height
        );
        this.clipPath.closePath();
      }
    }

    endClip() {
      const savedTransform = this.drawingContext.getTransform();
      this.drawingContext.setTransform(this._clipBaseTransform);
      this.drawingContext.clip(this.clipPath);
      this.drawingContext.setTransform(savedTransform);

      this.clipPath = null;

      super.endClip();

      this.drawingContext.fillStyle = this.states._cachedFillStyle;
      this.drawingContext.strokeStyle = this.states._cachedStrokeStyle;

      this.blendMode(this._cachedBlendMode);
    }

    //////////////////////////////////////////////
    // IMAGE | Loading & Displaying
    //////////////////////////////////////////////

    image(
      img,
      sx,
      sy,
      sWidth,
      sHeight,
      dx,
      dy,
      dWidth,
      dHeight
    ) {
      let cnv;
      if (img.gifProperties) {
        img._animateGif(this._pInst);
      }

      try {
        if (img instanceof MediaElement) {
          img._ensureCanvas();
        }
        if (this.states.tint && img.canvas) {
          cnv = this._getTintedImageCanvas(img);
        }
        if (!cnv) {
          cnv = img.canvas || img.elt;
        }
        let s = 1;
        if (img.width && img.width > 0) {
          s = cnv.width / img.width;
        }
        if (this._isErasing) {
          this.blendMode(this._cachedBlendMode);
        }

        this.drawingContext.drawImage(
          cnv,
          s * sx,
          s * sy,
          s * sWidth,
          s * sHeight,
          dx,
          dy,
          dWidth,
          dHeight
        );
        if (this._isErasing) {
          this._pInst.erase();
        }
      } catch (e) {
        if (e.name !== 'NS_ERROR_NOT_AVAILABLE') {
          throw e;
        }
      }
    }

    _getTintedImageCanvas(img) {
      if (!img.canvas) {
        return img;
      }

      if (!img.tintCanvas) {
        // Once an image has been tinted, keep its tint canvas
        // around so we don't need to re-incur the cost of
        // creating a new one for each tint
        img.tintCanvas = document.createElement('canvas');
      }

      // Keep the size of the tint canvas up-to-date
      if (img.tintCanvas.width !== img.canvas.width) {
        img.tintCanvas.width = img.canvas.width;
      }
      if (img.tintCanvas.height !== img.canvas.height) {
        img.tintCanvas.height = img.canvas.height;
      }

      // Goal: multiply the r,g,b,a values of the source by
      // the r,g,b,a values of the tint color
      const ctx = img.tintCanvas.getContext('2d');

      ctx.save();
      ctx.clearRect(0, 0, img.canvas.width, img.canvas.height);

      if (
        this.states.tint[0] < 255 ||
        this.states.tint[1] < 255 ||
        this.states.tint[2] < 255
      ) {
        // Color tint: we need to use the multiply blend mode to change the colors.
        // However, the canvas implementation of this destroys the alpha channel of
        // the image. To accommodate, we first get a version of the image with full
        // opacity everywhere, tint using multiply, and then use the destination-in
        // blend mode to restore the alpha channel again.

        // Start with the original image
        ctx.drawImage(img.canvas, 0, 0);

        // This blend mode makes everything opaque but forces the luma to match
        // the original image again
        ctx.globalCompositeOperation = 'luminosity';
        ctx.drawImage(img.canvas, 0, 0);

        // This blend mode forces the hue and chroma to match the original image.
        // After this we should have the original again, but with full opacity.
        ctx.globalCompositeOperation = 'color';
        ctx.drawImage(img.canvas, 0, 0);

        // Apply color tint
        ctx.globalCompositeOperation = 'multiply';
        ctx.fillStyle = `rgb(${this.states.tint.slice(0, 3).join(', ')})`;
        ctx.fillRect(0, 0, img.canvas.width, img.canvas.height);

        // Replace the alpha channel with the original alpha * the alpha tint
        ctx.globalCompositeOperation = 'destination-in';
        ctx.globalAlpha = this.states.tint[3] / 255;
        ctx.drawImage(img.canvas, 0, 0);
      } else {
        // If we only need to change the alpha, we can skip all the extra work!
        ctx.globalAlpha = this.states.tint[3] / 255;
        ctx.drawImage(img.canvas, 0, 0);
      }

      ctx.restore();
      return img.tintCanvas;
    }

    //////////////////////////////////////////////
    // IMAGE | Pixels
    //////////////////////////////////////////////

    blendMode(mode) {
      if (mode === SUBTRACT) {
        console.warn('blendMode(SUBTRACT) only works in WEBGL mode.');
      } else if (
        mode === BLEND ||
        mode === REMOVE ||
        mode === DARKEST ||
        mode === LIGHTEST ||
        mode === DIFFERENCE ||
        mode === MULTIPLY ||
        mode === EXCLUSION ||
        mode === SCREEN ||
        mode === REPLACE ||
        mode === OVERLAY ||
        mode === HARD_LIGHT ||
        mode === SOFT_LIGHT ||
        mode === DODGE ||
        mode === BURN ||
        mode === ADD
      ) {
        this._cachedBlendMode = mode;
        this.drawingContext.globalCompositeOperation = mode;
      } else {
        throw new Error(`Mode ${mode} not recognized.`);
      }
    }

    blend(...args) {
      const currBlend = this.drawingContext.globalCompositeOperation;
      const blendMode = args[args.length - 1];

      const copyArgs = Array.prototype.slice.call(args, 0, args.length - 1);

      this.drawingContext.globalCompositeOperation = blendMode;

      p5$2.prototype.copy.apply(this, copyArgs);

      this.drawingContext.globalCompositeOperation = currBlend;
    }

    // p5.Renderer2D.prototype.get = p5.Renderer.prototype.get;
    // .get() is not overridden

    // x,y are canvas-relative (pre-scaled by _pixelDensity)
    _getPixel(x, y) {
      let imageData, index;
      imageData = this.drawingContext.getImageData(x, y, 1, 1).data;
      index = 0;
      return [
        imageData[index + 0],
        imageData[index + 1],
        imageData[index + 2],
        imageData[index + 3]
      ];
    }

    loadPixels() {
      const pd = this._pixelDensity;
      const w = this.width * pd;
      const h = this.height * pd;
      const imageData = this.drawingContext.getImageData(0, 0, w, h);
      // @todo this should actually set pixels per object, so diff buffers can
      // have diff pixel arrays.
      this.imageData = imageData;
      this.pixels = imageData.data;
    }

    set(x, y, imgOrCol) {
      // round down to get integer numbers
      x = Math.floor(x);
      y = Math.floor(y);
      if (imgOrCol instanceof Graphics || imgOrCol instanceof Image) {
        this.drawingContext.save();
        this.drawingContext.setTransform(1, 0, 0, 1, 0, 0);
        this.drawingContext.scale(
          this._pixelDensity,
          this._pixelDensity
        );
        const width = imgOrCol.width;
        const height = imgOrCol.height;
        this.drawingContext.clearRect(x, y, width, height);
        this.drawingContext.drawImage(imgOrCol.canvas, x, y, width, height);
      } else {
        let r = 0,
          g = 0,
          b = 0,
          a = 0;
        let idx =
          4 *
          (y *
            this._pixelDensity *
            (this.width * this._pixelDensity) +
            x * this._pixelDensity);
        if (!this.imageData) {
          this.loadPixels();
        }
        if (typeof imgOrCol === 'number') {
          if (idx < this.pixels.length) {
            r = imgOrCol;
            g = imgOrCol;
            b = imgOrCol;
            a = 255;
            //this.updatePixels.call(this);
          }
        } else if (Array.isArray(imgOrCol)) {
          if (imgOrCol.length < 4) {
            throw new Error('pixel array must be of the form [R, G, B, A]');
          }
          if (idx < this.pixels.length) {
            r = imgOrCol[0];
            g = imgOrCol[1];
            b = imgOrCol[2];
            a = imgOrCol[3];
            //this.updatePixels.call(this);
          }
        } else if (imgOrCol instanceof p5$2.Color) {
          if (idx < this.pixels.length) {
            [r, g, b, a] = imgOrCol._getRGBA([255, 255, 255, 255]);
            //this.updatePixels.call(this);
          }
        }
        // loop over pixelDensity * pixelDensity
        for (let i = 0; i < this._pixelDensity; i++) {
          for (let j = 0; j < this._pixelDensity; j++) {
            // loop over
            idx =
              4 *
              ((y * this._pixelDensity + j) *
                this.width *
                this._pixelDensity +
                (x * this._pixelDensity + i));
            this.pixels[idx] = r;
            this.pixels[idx + 1] = g;
            this.pixels[idx + 2] = b;
            this.pixels[idx + 3] = a;
          }
        }
      }
    }

    updatePixels(x, y, w, h) {
      const pd = this._pixelDensity;
      if (
        x === undefined &&
        y === undefined &&
        w === undefined &&
        h === undefined
      ) {
        x = 0;
        y = 0;
        w = this.width;
        h = this.height;
      }
      x *= pd;
      y *= pd;
      w *= pd;
      h *= pd;

      if (this.gifProperties) {
        this.gifProperties.frames[this.gifProperties.displayIndex].image =
          this.imageData;
      }

      this.drawingContext.putImageData(this.imageData, 0, 0, x, y, w, h);
    }

    //////////////////////////////////////////////
    // SHAPE | 2D Primitives
    //////////////////////////////////////////////

    /*
     * This function requires that:
     *
     *   0 <= start < TWO_PI
     *
     *   start <= stop < start + TWO_PI
     */
    arc(x, y, w, h, start, stop, mode) {
      const ctx = this.drawingContext;

      const centerX = x + w / 2,
        centerY = y + h / 2,
        radiusX = w / 2,
        radiusY = h / 2;
      if (this._clipping) {
        const tempPath = new Path2D();
        tempPath.ellipse(centerX, centerY, radiusX, radiusY, 0, start, stop);
        const currentTransform = this.drawingContext.getTransform();
        const clipBaseTransform = this._clipBaseTransform.inverse();
        const relativeTransform = clipBaseTransform.multiply(currentTransform);
        this.clipPath.addPath(tempPath, relativeTransform);
        return this;
      }
      // Determines whether to add a line to the center, which should be done
      // when the mode is PIE or default; as well as when the start and end
      // angles do not form a full circle.
      const createPieSlice = ! (
        mode === CHORD ||
        mode === OPEN ||
        (stop - start) % TWO_PI === 0
      );

      // Fill curves
      if (this.states.fillColor) {
        ctx.beginPath();
        ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, start, stop);
        if (createPieSlice) ctx.lineTo(centerX, centerY);
        ctx.closePath();
        ctx.fill();
      }

      // Stroke curves
      if (this.states.strokeColor) {
        ctx.beginPath();
        ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, start, stop);

        if (mode === PIE && createPieSlice) {
          // In PIE mode, stroke is added to the center and back to path,
          // unless the pie forms a complete ellipse (see: createPieSlice)
          ctx.lineTo(centerX, centerY);
        }

        if (mode === PIE || mode === CHORD) {
          // Stroke connects back to path begin for both PIE and CHORD
          ctx.closePath();
        }
        ctx.stroke();
      }

      return this;

    }

    ellipse(args) {
      const ctx = this.drawingContext;
      const doFill = !!this.states.fillColor,
        doStroke = this.states.strokeColor;
      const x = parseFloat(args[0]),
        y = parseFloat(args[1]),
        w = parseFloat(args[2]),
        h = parseFloat(args[3]);
      if (doFill && !doStroke) {
        if (this._getFill() === styleEmpty) {
          return this;
        }
      } else if (!doFill && doStroke) {
        if (this._getStroke() === styleEmpty) {
          return this;
        }
      }
      const centerX = x + w / 2,
        centerY = y + h / 2,
        radiusX = w / 2,
        radiusY = h / 2;
      if (this._clipping) {
        const tempPath = new Path2D();
        tempPath.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI);
        const currentTransform = this.drawingContext.getTransform();
        const clipBaseTransform = this._clipBaseTransform.inverse();
        const relativeTransform = clipBaseTransform.multiply(currentTransform);
        this.clipPath.addPath(tempPath, relativeTransform);
        return this;
      }
      ctx.beginPath();
      ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI);
      ctx.closePath();
      if (doFill) {
        ctx.fill();
      }
      if (doStroke) {
        ctx.stroke();
      }

      return this;
    }

    line(x1, y1, x2, y2) {
      const ctx = this.drawingContext;
      if (!this.states.strokeColor) {
        return this;
      } else if (this._getStroke() === styleEmpty) {
        return this;
      }
      if (this._clipping) {
        const tempPath = new Path2D();
        tempPath.moveTo(x1, y1);
        tempPath.lineTo(x2, y2);
        const currentTransform = this.drawingContext.getTransform();
        const clipBaseTransform = this._clipBaseTransform.inverse();
        const relativeTransform = clipBaseTransform.multiply(currentTransform);
        this.clipPath.addPath(tempPath, relativeTransform);
        return this;
      }
      ctx.beginPath();
      ctx.moveTo(x1, y1);
      ctx.lineTo(x2, y2);
      ctx.stroke();

      return this;
    }

    point(x, y) {
      const ctx = this.drawingContext;
      if (!this.states.strokeColor) {
        return this;
      } else if (this._getStroke() === styleEmpty) {
        return this;
      }
      const s = this._getStroke();
      const f = this._getFill();
      if (this._clipping) {
        const tempPath = new Path2D();
        const drawingContextWidth = this.drawingContext.lineWidth;
        tempPath.arc(x, y, drawingContextWidth / 2, 0, TWO_PI);
        const currentTransform = this.drawingContext.getTransform();
        const clipBaseTransform = this._clipBaseTransform.inverse();
        const relativeTransform = clipBaseTransform.multiply(currentTransform);
        this.clipPath.addPath(tempPath, relativeTransform);
        return this;
      }
      this._setFill(s);
      ctx.beginPath();
      ctx.arc(x, y, ctx.lineWidth / 2, 0, TWO_PI, false);
      ctx.fill();
      this._setFill(f);

      return this;
    }

    quad(x1, y1, x2, y2, x3, y3, x4, y4) {
      const ctx = this.drawingContext;
      const doFill = !!this.states.fillColor,
        doStroke = this.states.strokeColor;
      if (doFill && !doStroke) {
        if (this._getFill() === styleEmpty) {
          return this;
        }
      } else if (!doFill && doStroke) {
        if (this._getStroke() === styleEmpty) {
          return this;
        }
      }
      if (this._clipping) {
        const tempPath = new Path2D();
        tempPath.moveTo(x1, y1);
        tempPath.lineTo(x2, y2);
        tempPath.lineTo(x3, y3);
        tempPath.lineTo(x4, y4);
        tempPath.closePath();
        const currentTransform = this.drawingContext.getTransform();
        const clipBaseTransform = this._clipBaseTransform.inverse();
        const relativeTransform = clipBaseTransform.multiply(currentTransform);
        this.clipPath.addPath(tempPath, relativeTransform);
        return this;
      }
      ctx.beginPath();
      ctx.moveTo(x1, y1);
      ctx.lineTo(x2, y2);
      ctx.lineTo(x3, y3);
      ctx.lineTo(x4, y4);
      ctx.closePath();
      if (doFill) {
        ctx.fill();
      }
      if (doStroke) {
        ctx.stroke();
      }
      return this;
    }

    rect(args) {
      const x = args[0];
      const y = args[1];
      const w = args[2];
      const h = args[3];
      let tl = args[4];
      let tr = args[5];
      let br = args[6];
      let bl = args[7];
      const ctx = this.drawingContext;
      const doFill = !!this.states.fillColor,
        doStroke = this.states.strokeColor;
      if (doFill && !doStroke) {
        if (this._getFill() === styleEmpty) {
          return this;
        }
      } else if (!doFill && doStroke) {
        if (this._getStroke() === styleEmpty) {
          return this;
        }
      }
      if (this._clipping) {
        const tempPath = new Path2D();
        if (typeof tl === 'undefined') {
          tempPath.rect(x, y, w, h);
        } else {
          tempPath.roundRect(x, y, w, h, [tl, tr, br, bl]);
        }
        const currentTransform = this.drawingContext.getTransform();
        const clipBaseTransform = this._clipBaseTransform.inverse();
        const relativeTransform = clipBaseTransform.multiply(currentTransform);
        this.clipPath.addPath(tempPath, relativeTransform);
        return this;
      }
      ctx.beginPath();
      if (typeof tl === 'undefined') {
        // No rounded corners
        ctx.rect(x, y, w, h);
      } else {
        // At least one rounded corner
        // Set defaults when not specified
        if (typeof tr === 'undefined') {
          tr = tl;
        }
        if (typeof br === 'undefined') {
          br = tr;
        }
        if (typeof bl === 'undefined') {
          bl = br;
        }

        // corner rounding must always be positive
        const absW = Math.abs(w);
        const absH = Math.abs(h);
        const hw = absW / 2;
        const hh = absH / 2;

        // Clip radii
        if (absW < 2 * tl) {
          tl = hw;
        }
        if (absH < 2 * tl) {
          tl = hh;
        }
        if (absW < 2 * tr) {
          tr = hw;
        }
        if (absH < 2 * tr) {
          tr = hh;
        }
        if (absW < 2 * br) {
          br = hw;
        }
        if (absH < 2 * br) {
          br = hh;
        }
        if (absW < 2 * bl) {
          bl = hw;
        }
        if (absH < 2 * bl) {
          bl = hh;
        }

        ctx.roundRect(x, y, w, h, [tl, tr, br, bl]);
      }
      if (doFill) {
        ctx.fill();
      }
      if (doStroke) {
        ctx.stroke();
      }
      return this;
    }


    triangle(args) {
      const ctx = this.drawingContext;
      const doFill = !!this.states.fillColor,
        doStroke = this.states.strokeColor;
      const x1 = args[0],
        y1 = args[1];
      const x2 = args[2],
        y2 = args[3];
      const x3 = args[4],
        y3 = args[5];
      if (doFill && !doStroke) {
        if (this._getFill() === styleEmpty) {
          return this;
        }
      } else if (!doFill && doStroke) {
        if (this._getStroke() === styleEmpty) {
          return this;
        }
      }
      if (this._clipping) {
        const tempPath = new Path2D();
        tempPath.moveTo(x1, y1);
        tempPath.lineTo(x2, y2);
        tempPath.lineTo(x3, y3);
        tempPath.closePath();
        const currentTransform = this.drawingContext.getTransform();
        const clipBaseTransform = this._clipBaseTransform.inverse();
        const relativeTransform = clipBaseTransform.multiply(currentTransform);
        this.clipPath.addPath(tempPath, relativeTransform);
        return this;
      }
      ctx.beginPath();
      ctx.moveTo(x1, y1);
      ctx.lineTo(x2, y2);
      ctx.lineTo(x3, y3);
      ctx.closePath();
      if (doFill) {
        ctx.fill();
      }
      if (doStroke) {
        ctx.stroke();
      }

      return this;
    }

    //////////////////////////////////////////////
    // SHAPE | Attributes
    //////////////////////////////////////////////

    strokeCap(cap) {
      if (
        cap === ROUND ||
        cap === SQUARE ||
        cap === PROJECT
      ) {
        this.drawingContext.lineCap = cap;
      }
      return this;
    }

    strokeJoin(join) {
      if (
        join === ROUND ||
        join === BEVEL ||
        join === MITER
      ) {
        this.drawingContext.lineJoin = join;
      }
      return this;
    }

    strokeWeight(w) {
      super.strokeWeight(w);
      if (typeof w === 'undefined' || w === 0) {
        // hack because lineWidth 0 doesn't work
        this.drawingContext.lineWidth = 0.0001;
      } else {
        this.drawingContext.lineWidth = w;
      }
      return this;
    }

    _getFill() {
      if (!this.states._cachedFillStyle) {
        this.states.setValue('_cachedFillStyle', this.drawingContext.fillStyle);
      }
      return this.states._cachedFillStyle;
    }

    _setFill(fillStyle) {
      if (fillStyle !== this.states._cachedFillStyle) {
        this.drawingContext.fillStyle = fillStyle;
        this.states.setValue('_cachedFillStyle', fillStyle);
      }
    }

    _getStroke() {
      if (!this.states._cachedStrokeStyle) {
        this.states.setValue('_cachedStrokeStyle', this.drawingContext.strokeStyle);
      }
      return this.states._cachedStrokeStyle;
    }

    _setStroke(strokeStyle) {
      if (strokeStyle !== this.states._cachedStrokeStyle) {
        this.drawingContext.strokeStyle = strokeStyle;
        this.states.setValue('_cachedStrokeStyle', strokeStyle);
      }
    }

    //////////////////////////////////////////////
    // TRANSFORM
    //////////////////////////////////////////////

    applyMatrix(a, b, c, d, e, f) {
      this.drawingContext.transform(a, b, c, d, e, f);
    }

    getWorldToScreenMatrix() {
      let domMatrix = new DOMMatrix()
        .scale(1 / this._pixelDensity)
        .multiply(this.drawingContext.getTransform());
      return new Matrix(domMatrix.toFloat32Array());
    }

    resetMatrix() {
      this.drawingContext.setTransform(1, 0, 0, 1, 0, 0);
      this.drawingContext.scale(
        this._pixelDensity,
        this._pixelDensity
      );
      return this;
    }

    rotate(rad) {
      this.drawingContext.rotate(rad);
    }

    scale(x, y) {
      this.drawingContext.scale(x, y);
      return this;
    }

    translate(x, y) {
      // support passing a vector as the 1st parameter
      if (x instanceof p5$2.Vector) {
        y = x.y;
        x = x.x;
      }
      this.drawingContext.translate(x, y);
      return this;
    }

    //////////////////////////////////////////////
    // TYPOGRAPHY (see src/type/textCore.js)
    //////////////////////////////////////////////

    //////////////////////////////////////////////
    // STRUCTURE
    //////////////////////////////////////////////

    // a push() operation is in progress.
    // the renderer should return a 'style' object that it wishes to
    // store on the push stack.
    // derived renderers should call the base class' push() method
    // to fetch the base style object.
    push() {
      this.drawingContext.save();

      // get the base renderer style
      return super.push();
    }

    // a pop() operation is in progress
    // the renderer is passed the 'style' object that it returned
    // from its push() method.
    // derived renderers should pass this object to their base
    // class' pop method
    pop(style) {
      this.drawingContext.restore();

      super.pop(style);
    }

    // Text support methods
    textCanvas() {
      return this.canvas;
    }

    textDrawingContext() {
      return this.drawingContext;
    }

    _renderText(text, x, y, maxY, minY) {
      let states = this.states;
      let context = this.textDrawingContext();

      if (y < minY || y >= maxY) {
        return; // don't render lines beyond minY/maxY
      }

      this.push();

      // no stroke unless specified by user
      if (states.strokeColor && states.strokeSet) {
        context.strokeText(text, x, y);
      }

      if (!this._clipping && states.fillColor) {

        // if fill hasn't been set by user, use default text fill
        if (!states.fillSet) {
          this._setFill(DefaultFill);
        }
        context.fillText(text, x, y);
      }

      this.pop();
    }

    /*
      Position the lines of text based on their textAlign/textBaseline properties
    */
    _positionLines(x, y, width, height, lines) {
      let { textLeading, textAlign } = this.states;
      let adjustedX, lineData = new Array(lines.length);
      let adjustedW = typeof width === 'undefined' ? 0 : width;
      let adjustedH = typeof height === 'undefined' ? 0 : height;

      for (let i = 0; i < lines.length; i++) {
        switch (textAlign) {
          case textCoreConstants.START:
            throw new Error('textBounds: START not yet supported for textAlign'); // default to LEFT
          case LEFT:
            adjustedX = x;
            break;
          case CENTER:
            adjustedX = x + adjustedW / 2;
            break;
          case RIGHT:
            adjustedX = x + adjustedW;
            break;
          case textCoreConstants.END:
            throw new Error('textBounds: END not yet supported for textAlign');
        }
        lineData[i] = { text: lines[i], x: adjustedX, y: y + i * textLeading };
      }

      return this._yAlignOffset(lineData, adjustedH);
    }

    /*
      Get the y-offset for text given the height, leading, line-count and textBaseline property
    */
    _yAlignOffset(dataArr, height) {
      if (typeof height === 'undefined') {
        throw Error('_yAlignOffset: height is required');
      }

      let { textLeading, textBaseline } = this.states;
      let yOff = 0, numLines = dataArr.length;
      let ydiff = height - (textLeading * (numLines - 1));

      switch (textBaseline) { // drawingContext ?
        case TOP:
          break; // ??
        case BASELINE:
          break;
        case textCoreConstants._CTX_MIDDLE:
          yOff = ydiff / 2 + this._middleAlignOffset();
          break;
        case BOTTOM:
          yOff = ydiff;
          break;
        case textCoreConstants.IDEOGRAPHIC:
          console.warn('textBounds: IDEOGRAPHIC not yet supported for textBaseline'); // FES?
          break;
        case textCoreConstants.HANGING:
          console.warn('textBounds: HANGING not yet supported for textBaseline'); // FES?
          break;
      }

      dataArr.forEach(ele => ele.y += yOff);
      return dataArr;
    }
  }

  function renderer2D(p5, fn){
    /**
     * p5.Renderer2D
     * The 2D graphics canvas renderer class.
     * extends p5.Renderer
     * @private
     */
    p5.Renderer2D = Renderer2D;
    p5.renderers[P2D] = Renderer2D;
    p5.renderers['p2d-hdr'] = new Proxy(Renderer2D, {
      construct(target, [pInst, w, h, isMainCanvas, elt]){
        return new target(pInst, w, h, isMainCanvas, elt, { colorSpace: 'display-p3' });
      }
    });
  }

  /**
   * @module Structure
   * @submodule Structure
   * @for p5
   * @requires constants
   */


  /**
   * This is the p5 instance constructor.
   *
   * A p5 instance holds all the properties and methods related to
   * a p5 sketch.  It expects an incoming sketch closure and it can also
   * take an optional node parameter for attaching the generated p5 canvas
   * to a node.  The sketch closure takes the newly created p5 instance as
   * its sole argument and may optionally set an asynchronous function
   * using `async/await`, along with the standard <a href="#/p5/setup">setup()</a>,
   *  and/or <a href="#/p5/setup">setup()</a>, and/or <a href="#/p5/draw">draw()</a>
   *  properties on it for running a sketch.
   *
   * A p5 sketch can run in "global" or "instance" mode:
   * "global"   - all properties and methods are attached to the window
   * "instance" - all properties and methods are bound to this p5 object
   *
   * @class p5
   * @param  {function(p5)}       sketch a closure that can set optional <a href="#/p5/preload">preload()</a>,
   *                              <a href="#/p5/setup">setup()</a>, and/or <a href="#/p5/draw">draw()</a> properties on the
   *                              given p5 instance
   * @param  {String|HTMLElement}        [node] element to attach canvas to
   * @return {p5}                 a p5 instance
   */
  let p5$2 = class p5 {
    static VERSION = VERSION;
    // This is a pointer to our global mode p5 instance, if we're in
    // global mode.
    static instance = null;
    static lifecycleHooks = {
      presetup: [],
      postsetup: [],
      predraw: [],
      postdraw: [],
      remove: []
    };

    // FES stub
    static _checkForUserDefinedFunctions = () => {};
    static _friendlyFileLoadError = () => {};

    constructor(sketch, node) {
      // Apply addon defined decorations
      if(p5.decorations.size > 0){
        for (const [patternArray, decoration] of p5.decorations) {
          for(const member in p5.prototype) {
            // Member must be a function
            if (typeof p5.prototype[member] !== 'function') continue;

            if (!patternArray.some(pattern => {
              if (typeof pattern === 'string') {
                return pattern === member;
              } else if (pattern instanceof RegExp) {
                return pattern.test(member);
              }
            })) continue;

            p5.prototype[member] = decoration(p5.prototype[member], {
              kind: 'method',
              name: member,
              access: {},
              static: false,
              private: false,
              addInitializer(initializer){}
            });
          }
        }

        p5.decorations.clear();
      }

      //////////////////////////////////////////////
      // PRIVATE p5 PROPERTIES AND METHODS
      //////////////////////////////////////////////

      this.hitCriticalError = false;
      this._setupDone = false;
      this._userNode = node;
      this._curElement = null;
      this._elements = [];
      this._glAttributes = null;
      this._webgpuAttributes = null;
      this._requestAnimId = 0;
      this._isGlobal = false;
      this._loop = true;
      this._startListener = null;
      this._initializeInstanceVariables();
      this._events = {
      };
      this._removeAbortController = new AbortController();
      this._removeSignal = this._removeAbortController.signal;
      this._millisStart = -1;
      this._recording = false;

      // States used in the custom random generators
      this._lcg_random_state = null; // NOTE: move to random.js
      this._gaussian_previous = false; // NOTE: move to random.js

      // ensure correct reporting of window dimensions
      this._updateWindowSize();

      const bindGlobal = createBindGlobal(this);
      // If the user has created a global setup or draw function,
      // assume "global" mode and make everything global (i.e. on the window)
      if (!sketch) {
        this._isGlobal = true;
        if (window.hitCriticalError) {
          return;
        }
        p5.instance = this;

        // Loop through methods on the prototype and attach them to the window
        // All methods and properties with name starting with '_' will be skipped
        for (const p of Object.getOwnPropertyNames(p5.prototype)) {
          if(p[0] === '_') continue;
          bindGlobal(p);
        }

        const protectedProperties = ['constructor', 'length'];
        // Attach its properties to the window
        for (const p in this) {
          if (this.hasOwnProperty(p)) {
            if(p[0] === '_' || protectedProperties.includes(p)) continue;
            bindGlobal(p);
          }
        }
      } else {
        // Else, the user has passed in a sketch closure that may set
        // user-provided 'setup', 'draw', etc. properties on this instance of p5
        sketch(this);

        // Run a check to see if the user has misspelled 'setup', 'draw', etc
        // detects capitalization mistakes only ( Setup, SETUP, MouseClicked, etc)
        p5._checkForUserDefinedFunctions(this);
      }

      const focusHandler = () => {
        this.focused = true;
      };
      const blurHandler = () => {
        this.focused = false;
      };
      window.addEventListener('focus', focusHandler);
      window.addEventListener('blur', blurHandler);
      p5.lifecycleHooks.remove.push(function() {
        window.removeEventListener('focus', focusHandler);
        window.removeEventListener('blur', blurHandler);
      });

      // Initialization complete, start runtime
      if (document.readyState === 'complete') {
        this.#_start();
      } else {
        this._startListener = this.#_start.bind(this);
        window.addEventListener('load', this._startListener, false);
      }
    }

    get pixels(){
      return this._renderer.pixels;
    }

    get drawingContext(){
      return this._renderer.drawingContext;
    }

    static _registeredAddons = new Set();
    static registerAddon(addon) {
      const lifecycles = {};

      // Don't re-register an addon. This allows addons
      // to register dependency addons without worrying about
      // them getting double-added.
      if (p5._registeredAddons.has(addon)) return;
      p5._registeredAddons.add(addon);

      addon(p5, p5.prototype, lifecycles);

      const validLifecycles = Object.keys(p5.lifecycleHooks);
      for(const name of validLifecycles){
        if(typeof lifecycles[name] === 'function'){
          p5.lifecycleHooks[name].push(lifecycles[name]);
        }
      }
    }

    static decorations = new Map();
    static decorateHelper(pattern, decoration){
      let patternArray = pattern;
      if (!Array.isArray(pattern)) patternArray = [pattern];
      p5.decorations.set(patternArray, decoration);
    }

    #customActions = {};
    _customActions = new Proxy({}, {
      get: (target, prop) => {
        if(!this.#customActions[prop]){
          const context = this._isGlobal ? window : this;
          if(typeof context[prop] === 'function'){
            this.#customActions[prop] = context[prop].bind(this);
          }
        }

        return this.#customActions[prop];
      }
    });

    async #_start() {
      if (this.hitCriticalError) return;
      // Find node if id given
      if (this._userNode) {
        if (typeof this._userNode === 'string') {
          this._userNode = document.getElementById(this._userNode);
        }
      }

      await this.#_setup();
      if (this.hitCriticalError) return;
      if (!this._recording) {
        this._draw();
      }
    }

    async #_setup() {
      // Run `presetup` hooks
      await this._runLifecycleHook('presetup');
      if (this.hitCriticalError) return;

      // Always create a default canvas.
      // Later on if the user calls createCanvas, this default one
      // will be replaced
      this.createCanvas(
        100,
        100,
        P2D
      );

      // Record the time when setup starts. millis() will start at 0 within
      // setup, but this isn't documented, locked-in behavior yet.
      this._millisStart = window.performance.now();

      const context = this._isGlobal ? window : this;
      if (typeof context.setup === 'function') {
        await context.setup();
      }
      if (this.hitCriticalError) return;

      const canvases = document.getElementsByTagName('canvas');
      for (const k of canvases) {
        // Apply touchAction = 'none' to canvases to prevent scrolling
        // when dragging on canvas elements
        k.style.touchAction = 'none';

        // unhide any hidden canvases that were created
        if (k.dataset.hidden === 'true') {
          k.style.visibility = '';
          delete k.dataset.hidden;
        }
      }

      this._lastTargetFrameTime = window.performance.now();
      this._lastRealFrameTime = window.performance.now();
      this._setupDone = true;
      if (this._accessibleOutputs.grid || this._accessibleOutputs.text) {
        this._updateAccsOutput();
      }

      // Run `postsetup` hooks
      await this._runLifecycleHook('postsetup');

      // Record the time when the draw loop starts so that millis() starts at 0
      // when the draw loop begins.
      this._millisStart = window.performance.now();
    }

    // While '#_draw' here is async, it is not awaited as 'requestAnimationFrame'
    // does not await its callback. Thus it is not recommended for 'draw()` to be
    // async and use await within as the next frame may start rendering before the
    // current frame finish awaiting. The same goes for lifecycle hooks 'predraw'
    // and 'postdraw'.
    async _draw(requestAnimationFrameTimestamp) {
      if (this.hitCriticalError) return;
      const now = requestAnimationFrameTimestamp || window.performance.now();
      const timeSinceLastFrame = now - this._lastTargetFrameTime;
      const targetTimeBetweenFrames = 1000 / this._targetFrameRate;

      // only draw if we really need to; don't overextend the browser.
      // draw if we're within 5ms of when our next frame should paint
      // (this will prevent us from giving up opportunities to draw
      // again when it's really about time for us to do so). fixes an
      // issue where the frameRate is too low if our refresh loop isn't
      // in sync with the browser. note that we have to draw once even
      // if looping is off, so we bypass the time delay if that
      // is the case.
      const epsilon = 5;
      if (
        !this._loop ||
        timeSinceLastFrame >= targetTimeBetweenFrames - epsilon
      ) {
        //mandatory update values(matrixes and stack)
        this.deltaTime = now - this._lastRealFrameTime;
        this._frameRate = 1000.0 / this.deltaTime;
        await this.redraw();
        this._lastTargetFrameTime = Math.max(this._lastTargetFrameTime
          + targetTimeBetweenFrames, now);
        this._lastRealFrameTime = now;

        // If the user is actually using mouse module, then update
        // coordinates, otherwise skip. We can test this by simply
        // checking if any of the mouse functions are available or not.
        // NOTE : This reflects only in complete build or modular build.
        if (typeof this._updateMouseCoords !== 'undefined') {
          this._updateMouseCoords();

          //reset delta values so they reset even if there is no mouse event to set them
          // for example if the mouse is outside the screen
          this.movedX = 0;
          this.movedY = 0;
        }
      }

      // get notified the next time the browser gives us
      // an opportunity to draw.
      if (this._loop) {
        this._requestAnimId = window.requestAnimationFrame(
          this._draw.bind(this)
        );
      }
    }

    /**
     * Removes the sketch from the web page.
     *
     * Calling `remove()` stops the draw loop and removes any HTML elements
     * created by the sketch, including the canvas. A new sketch can be
     * created by using the <a href="#/p5/p5">p5()</a> constructor, as in
     * `new p5()`.
     *
     * @example
     * // Double-click to remove the canvas.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A white circle on a gray background. The circle follows the mouse as the user moves. The sketch disappears when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   // Paint the background repeatedly.
     *   background(200);
     *
     *   // Draw circles repeatedly.
     *   circle(mouseX, mouseY, 40);
     * }
     *
     * // Remove the sketch when the user double-clicks.
     * function doubleClicked() {
     *   remove();
     * }
     */
    async remove() {
      // Remove start listener to prevent orphan canvas being created
      if(this._startListener){
        window.removeEventListener('load', this._startListener, false);
      }

      if (this._curElement) {
        // stop draw
        this._loop = false;
        if (this._requestAnimId) {
          window.cancelAnimationFrame(this._requestAnimId);
        }

        // Send sketch remove signal
        this._removeAbortController.abort();

        // remove DOM elements created by p5
        for (const e of this._elements) {
          if (e.elt && e.elt.parentNode) {
            e.elt.parentNode.removeChild(e.elt);
          }
        }

        // Run `remove` hooks
        await this._runLifecycleHook('remove');
      }

      // remove window bound properties and methods
      if (this._isGlobal) {
        for (const p in p5.prototype) {
          try {
            delete window[p];
          } catch (x) {
            window[p] = undefined;
          }
        }
        for (const p2 in this) {
          if (this.hasOwnProperty(p2)) {
            try {
              delete window[p2];
            } catch (x) {
              window[p2] = undefined;
            }
          }
        }
        p5.instance = null;
      }
    }

    async _runLifecycleHook(hookName) {
      await Promise.all(p5.lifecycleHooks[hookName].map(hook => {
        return hook.call(this);
      }));
    }

    _initializeInstanceVariables() {
      this._accessibleOutputs = {
        text: false,
        grid: false,
        textLabel: false,
        gridLabel: false
      };

      this._styles = [];
      this._downKeys = {}; //Holds the key codes of currently pressed keys
      this._downKeyCodes = {};
    }
  };

  // Global helper function for binding properties to window in global mode
  function createBindGlobal(instance) {
    return function bindGlobal(property) {
      if (property === 'constructor') return;

      // Check if this property has a getter on the instance or prototype
      const instanceDescriptor = Object.getOwnPropertyDescriptor(
        instance,
        property
      );
      const prototypeDescriptor = Object.getOwnPropertyDescriptor(
        p5$2.prototype,
        property
      );
      const hasGetter = (instanceDescriptor && instanceDescriptor.get) ||
                       (prototypeDescriptor && prototypeDescriptor.get);

      // Only check if it's a function if it doesn't have a getter
      // to avoid actually evaluating getters before things like the
      // renderer are fully constructed
      let isPrototypeFunction = false;
      let isConstant = false;
      let constantValue;

      if (!hasGetter) {
        const prototypeValue = p5$2.prototype[property];
        isPrototypeFunction = typeof prototypeValue === 'function';

        // Check if this is a true constant from the constants module
        if (!isPrototypeFunction && constants[property] !== undefined) {
          isConstant = true;
          constantValue = prototypeValue;
        }
      }

      if (isPrototypeFunction) {
        // For regular functions, cache the bound function
        const boundFunction = p5$2.prototype[property].bind(instance);
        Object.defineProperty(window, property, {
          configurable: true,
          enumerable: true,
          value: boundFunction
        });
      } else if (isConstant) {
        // For constants, cache the value directly
        Object.defineProperty(window, property, {
          configurable: true,
          enumerable: true,
          value: constantValue
        });
      } else if (hasGetter || !isPrototypeFunction) {
        // For properties with getters or non-function properties, use lazy optimization
        // On first access, determine the type and optimize subsequent accesses
        let lastFunction = null;
        let boundFunction = null;
        let isFunction = null; // null = unknown, true = function, false = not function

        Object.defineProperty(window, property, {
          configurable: true,
          enumerable: true,
          get: () => {
            const currentValue = instance[property];

            if (isFunction === null) {
              // First access - determine type and optimize
              isFunction = typeof currentValue === 'function';
              if (isFunction) {
                lastFunction = currentValue;
                boundFunction = currentValue.bind(instance);
                return boundFunction;
              } else {
                return currentValue;
              }
            } else if (isFunction) {
              // Optimized function path - only rebind if function changed
              if (currentValue !== lastFunction) {
                lastFunction = currentValue;
                boundFunction = currentValue.bind(instance);
              }
              return boundFunction;
            } else {
              // Optimized non-function path
              return currentValue;
            }
          }
        });
      }
    };
  }

  // Attach constants to p5 prototype
  for (const k in constants) {
    p5$2.prototype[k] = constants[k];
  }

  p5$2.registerAddon(transform$1);
  p5$2.registerAddon(structure);
  p5$2.registerAddon(environment$1);
  p5$2.registerAddon(rendering);
  p5$2.registerAddon(renderer);
  p5$2.registerAddon(renderer2D);
  p5$2.registerAddon(graphics);

  //////////////////////////////////////////////
  // PUBLIC p5 PROPERTIES AND METHODS
  //////////////////////////////////////////////

  /**
   * A function that's called once when the sketch begins running.
   *
   * Declaring the function `setup()` sets a code block to run once
   * automatically when the sketch starts running. It's used to perform
   * setup tasks such as creating the canvas and initializing variables:
   *
   * ```js
   * function setup() {
   *   // Code to run once at the start of the sketch.
   * }
   * ```
   *
   * Code placed in `setup()` will run once before code placed in
   * <a href="#/p5/draw">draw()</a> begins looping.
   * If `setup()` is declared `async` (e.g. `async function setup()`),
   * execution pauses at each `await` until its promise resolves.
   * For example, `font = await loadFont(...)` waits for the font asset
   * to load because `loadFont()` function returns a promise, and the await
   * keyword means the program will wait for the promise to resolve.
   * This ensures that all assets are fully loaded before the sketch continues.
   *
   *
   * loading assets.
   *
   * Note: `setup()` doesn’t have to be declared, but it’s common practice to do so.
   *
   * @method setup
   * @for p5
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // Draw the circle.
   *   circle(50, 50, 40);
   *
   *   describe('A white circle on a gray background.');
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   // Paint the background once.
   *   background(200);
   *
   *   describe(
   *     'A white circle on a gray background. The circle follows the mouse as the user moves, leaving a trail.'
   *   );
   * }
   *
   * function draw() {
   *   // Draw circles repeatedly.
   *   circle(mouseX, mouseY, 40);
   * }
   *
   * @example
   * let img;
   *
   * async function setup() {
   *   img = await loadImage('assets/bricks.jpg');
   *
   *   createCanvas(100, 100);
   *
   *   // Draw the image.
   *   image(img, 0, 0);
   *
   *   describe(
   *     'A white circle on a brick wall. The circle follows the mouse as the user moves, leaving a trail.'
   *   );
   * }
   *
   * function draw() {
   *   // Style the circle.
   *   noStroke();
   *
   *   // Draw the circle.
   *   circle(mouseX, mouseY, 10);
   * }
   */
  /**
   * A function that's called repeatedly while the sketch runs.
   *
   * Declaring the function `draw()` sets a code block to run repeatedly
   * once the sketch starts. It’s used to create animations and respond to
   * user inputs:
   *
   * ```js
   * function draw() {
   *   // Code to run repeatedly.
   * }
   * ```
   *
   * This is often called the "draw loop" because p5.js calls the code in
   * `draw()` in a loop behind the scenes. By default, `draw()` tries to run
   * 60 times per second. The actual rate depends on many factors. The
   * drawing rate, called the "frame rate", can be controlled by calling
   * <a href="#/p5/frameRate">frameRate()</a>. The number of times `draw()`
   * has run is stored in the system variable
   * <a href="#/p5/frameCount">frameCount()</a>.
   *
   * Code placed within `draw()` begins looping after
   * <a href="#/p5/setup">setup()</a> runs. `draw()` will run until the user
   * closes the sketch. `draw()` can be stopped by calling the
   * <a href="#/p5/noLoop">noLoop()</a> function. `draw()` can be resumed by
   * calling the <a href="#/p5/loop">loop()</a> function.
   *
   * @method draw
   * @for p5
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   // Paint the background once.
   *   background(200);
   *
   *   describe(
   *     'A white circle on a gray background. The circle follows the mouse as the user moves, leaving a trail.'
   *   );
   * }
   *
   * function draw() {
   *   // Draw circles repeatedly.
   *   circle(mouseX, mouseY, 40);
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   describe(
   *     'A white circle on a gray background. The circle follows the mouse as the user moves.'
   *   );
   * }
   *
   * function draw() {
   *   // Paint the background repeatedly.
   *   background(200);
   *
   *   // Draw circles repeatedly.
   *   circle(mouseX, mouseY, 40);
   * }
   *
   * @example
   * // Double-click the canvas to change the circle's color.
   *
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   describe(
   *     'A white circle on a gray background. The circle follows the mouse as the user moves. The circle changes color to pink when the user double-clicks.'
   *   );
   * }
   *
   * function draw() {
   *   // Paint the background repeatedly.
   *   background(200);
   *
   *   // Draw circles repeatedly.
   *   circle(mouseX, mouseY, 40);
   * }
   *
   * // Change the fill color when the user double-clicks.
   * function doubleClicked() {
   *   fill('deeppink');
   * }
   */

  /**
   * Turns off the parts of the Friendly Error System (FES) that impact performance.
   *
   * The <a href="https://github.com/processing/p5.js/blob/main/contributor_docs/friendly_error_system.md" target="_blank">FES</a>
   * can cause sketches to draw slowly because it does extra work behind the
   * scenes. For example, the FES checks the arguments passed to functions,
   * which takes time to process. Disabling the FES can significantly improve
   * performance by turning off these checks.
   *
   * @static
   * @property {Boolean} disableFriendlyErrors
   *
   * @example
   * // Disable the FES.
   * p5.disableFriendlyErrors = true;
   *
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *
   *   // The circle() function requires three arguments. The
   *   // next line would normally display a friendly error that
   *   // points this out. Instead, nothing happens and it fails
   *   // silently.
   *   circle(50, 50);
   *
   *   describe('A gray square.');
   * }
   */

  /**
   * Loads a p5.js library.
   *
   * A library is a function that adds functionality to p5.js by adding methods
   * and properties for sketches to use, or for automatically running code at
   * different stages of the p5.js lifecycle. Take a look at the
   * <a href="/contribute/creating_libraries/">contributor docs for creating libraries</a>
   * to learn more about creating libraries.
   *
   * @static
   * @method registerAddon
   * @param {Function} library The library function to register
   *
   * @example
   * function myAddon(p5, fn, lifecycles) {
   *   fn.sayHello = function() {
   *     this.textAlign(this.CENTER, this.CENTER);
   *     this.text('Hello!', this.width / 2, this.height / 2);
   *   };
   * }
   * p5.registerAddon(myAddon);
   *
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   background(200);
   *   sayHello(); // The sayHello method is now available!
   *
   *   describe('The text "Hello!"');
   * }
   */

  function shape(p5){
    p5.registerAddon(primitives);
    p5.registerAddon(attributes);
    p5.registerAddon(curves);
    p5.registerAddon(vertex);
    p5.registerAddon(customShapes);
  }

  /**
   * @module Environment
   * @submodule Environment
   * @for p5
   * @requires core
   */

  function describe(p5, fn){
    const descContainer = '_Description'; //Fallback container
    const fallbackDescId = '_fallbackDesc'; //Fallback description
    const fallbackTableId = '_fallbackTable'; //Fallback Table
    const fallbackTableElId = '_fte_'; //Fallback Table Element
    const labelContainer = '_Label'; //Label container
    const labelDescId = '_labelDesc'; //Label description
    const labelTableId = '_labelTable'; //Label Table
    const labelTableElId = '_lte_'; //Label Table Element

    /**
     * Creates a screen reader-accessible description of the canvas.
     *
     * The first parameter, `text`, is the description of the canvas.
     *
     * The second parameter, `display`, is optional. It determines how the
     * description is displayed. If `LABEL` is passed, as in
     * `describe('A description.', LABEL)`, the description will be visible in
     * a div element next to the canvas. If `FALLBACK` is passed, as in
     * `describe('A description.', FALLBACK)`, the description will only be
     * visible to screen readers. This is the default mode.
     *
     * Read
     * <a href="/learn/accessible-labels.html">Writing accessible canvas descriptions</a>
     * to learn more about making sketches accessible.
     *
     * @method describe
     * @param  {String} text        description of the canvas.
     * @param  {(FALLBACK|LABEL)} [display] either LABEL or FALLBACK.
     *
     * @example
     * function setup() {
     *   background('pink');
     *
     *   // Draw a heart.
     *   fill('red');
     *   noStroke();
     *   circle(67, 67, 20);
     *   circle(83, 67, 20);
     *   triangle(91, 73, 75, 95, 59, 73);
     *
     *   // Add a general description of the canvas.
     *   describe('A pink square with a red heart in the bottom-right corner.');
     * }
     *
     * @example
     * function setup() {
     *   background('pink');
     *
     *   // Draw a heart.
     *   fill('red');
     *   noStroke();
     *   circle(67, 67, 20);
     *   circle(83, 67, 20);
     *   triangle(91, 73, 75, 95, 59, 73);
     *
     *   // Add a general description of the canvas
     *   // and display it for debugging.
     *   describe('A pink square with a red heart in the bottom-right corner.', LABEL);
     * }
     *
     * @example
     * function setup(){
     *  createCanvas(100, 100);
     * };
     *
     * function draw() {
     *   background(200);
     *
     *   // The expression
     *   // frameCount % 100
     *   // causes x to increase from 0
     *   // to 99, then restart from 0.
     *   let x = frameCount % 100;
     *
     *   // Draw the circle.
     *   fill(0, 255, 0);
     *   circle(x, 50, 40);
     *
     *   // Add a general description of the canvas.
     *   describe(`A green circle at (${x}, 50) moves from left to right on a gray square.`);
     * }
     *
     * @example
     * function setup(){
     * createCanvas(100, 100);
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // The expression
     *   // frameCount % 100
     *   // causes x to increase from 0
     *   // to 99, then restart from 0.
     *   let x = frameCount % 100;
     *
     *   // Draw the circle.
     *   fill(0, 255, 0);
     *   circle(x, 50, 40);
     *
     *   // Add a general description of the canvas
     *   // and display it for debugging.
     *   describe(`A green circle at (${x}, 50) moves from left to right on a gray square.`, LABEL);
     * }
     */
    fn.describe = function(text, display) {
      // p5._validateParameters('describe', arguments);
      if (typeof text !== 'string') {
        return;
      }
      const cnvId = this.canvas.id;
      //calls function that adds punctuation for better screen reading
      text = _descriptionText(text);
      //if there is no dummyDOM
      if (!this.dummyDOM) {
        this.dummyDOM = document.getElementById(cnvId).parentNode;
      }
      if (!this.descriptions) {
        this.descriptions = {};
      }
      //check if html structure for description is ready
      if (this.descriptions.fallback) {
        //check if text is different from current description
        if (this.descriptions.fallback.innerHTML !== text) {
          //update description
          this.descriptions.fallback.innerHTML = text;
        }
      } else {
        //create fallback html structure
        this._describeHTML('fallback', text);
      }
      //if display is LABEL
      if (display === this.LABEL) {
        //check if html structure for label is ready
        if (this.descriptions.label) {
          //check if text is different from current label
          if (this.descriptions.label.innerHTML !== text) {
            //update label description
            this.descriptions.label.innerHTML = text;
          }
        } else {
          //create label html structure
          this._describeHTML('label', text);
        }
      }
    };

    /**
     * Creates a screen reader-accessible description of elements in the canvas.
     *
     * Elements are shapes or groups of shapes that create meaning together. For
     * example, a few overlapping circles could make an "eye" element.
     *
     * The first parameter, `name`, is the name of the element.
     *
     * The second parameter, `text`, is the description of the element.
     *
     * The third parameter, `display`, is optional. It determines how the
     * description is displayed. If `LABEL` is passed, as in
     * `describe('A description.', LABEL)`, the description will be visible in
     * a div element next to the canvas. Using `LABEL` creates unhelpful
     * duplicates for screen readers. Only use `LABEL` during development. If
     * `FALLBACK` is passed, as in `describe('A description.', FALLBACK)`, the
     * description will only be visible to screen readers. This is the default
     * mode.
     *
     * Read
     * <a href="/learn/accessible-labels.html">Writing accessible canvas descriptions</a>
     * to learn more about making sketches accessible.
     *
     * @method describeElement
     * @param  {String} name        name of the element.
     * @param  {String} text        description of the element.
     * @param  {(FALLBACK|LABEL)} [display] either LABEL or FALLBACK.
     *
     * @example
     * function setup() {
     *   background('pink');
     *
     *   // Describe the first element
     *   // and draw it.
     *   describeElement('Circle', 'A yellow circle in the top-left corner.');
     *   noStroke();
     *   fill('yellow');
     *   circle(25, 25, 40);
     *
     *   // Describe the second element
     *   // and draw it.
     *   describeElement('Heart', 'A red heart in the bottom-right corner.');
     *   fill('red');
     *   circle(66.6, 66.6, 20);
     *   circle(83.2, 66.6, 20);
     *   triangle(91.2, 72.6, 75, 95, 58.6, 72.6);
     *
     *   // Add a general description of the canvas.
     *   describe('A red heart and yellow circle over a pink background.');
     * }
     *
     * @example
     * function setup() {
     *   background('pink');
     *
     *   // Describe the first element
     *   // and draw it. Display the
     *   // description for debugging.
     *   describeElement('Circle', 'A yellow circle in the top-left corner.', LABEL);
     *   noStroke();
     *   fill('yellow');
     *   circle(25, 25, 40);
     *
     *   // Describe the second element
     *   // and draw it. Display the
     *   // description for debugging.
     *   describeElement('Heart', 'A red heart in the bottom-right corner.', LABEL);
     *   fill('red');
     *   circle(66.6, 66.6, 20);
     *   circle(83.2, 66.6, 20);
     *   triangle(91.2, 72.6, 75, 95, 58.6, 72.6);
     *
     *   // Add a general description of the canvas.
     *   describe('A red heart and yellow circle over a pink background.');
     * }
     */

    fn.describeElement = function(name, text, display) {
      // p5._validateParameters('describeElement', arguments);
      if (typeof text !== 'string' || typeof name !== 'string') {
        return;
      }
      const cnvId = this.canvas.id;
      //calls function that adds punctuation for better screen reading
      text = _descriptionText(text);
      //calls function that adds punctuation for better screen reading
      let elementName = _elementName(name);
      //remove any special characters from name to use it as html id
      name = name.replace(/[^a-zA-Z0-9]/g, '');

      //store element description
      let inner = `<th scope="row">${elementName}</th><td>${text}</td>`;
      //if there is no dummyDOM
      if (!this.dummyDOM) {
        this.dummyDOM = document.getElementById(cnvId).parentNode;
      }
      if (!this.descriptions) {
        this.descriptions = { fallbackElements: {} };
      } else if (!this.descriptions.fallbackElements) {
        this.descriptions.fallbackElements = {};
      }
      //check if html structure for element description is ready
      if (this.descriptions.fallbackElements[name]) {
        //if current element description is not the same as inner
        if (this.descriptions.fallbackElements[name].innerHTML !== inner) {
          //update element description
          this.descriptions.fallbackElements[name].innerHTML = inner;
        }
      } else {
        //create fallback html structure
        this._describeElementHTML('fallback', name, inner);
      }
      //if display is LABEL
      if (display === this.LABEL) {
        if (!this.descriptions.labelElements) {
          this.descriptions.labelElements = {};
        }
        //if html structure for label element description is ready
        if (this.descriptions.labelElements[name]) {
          //if label element description is different
          if (this.descriptions.labelElements[name].innerHTML !== inner) {
            //update label element description
            this.descriptions.labelElements[name].innerHTML = inner;
          }
        } else {
          //create label element html structure
          this._describeElementHTML('label', name, inner);
        }
      }
    };

    /*
     *
     * Helper functions for describe() and describeElement().
     *
     */

    // check that text is not LABEL or FALLBACK and ensure text ends with punctuation mark
    function _descriptionText(text) {
      if (text === 'label' || text === 'fallback') {
        throw new Error('description should not be LABEL or FALLBACK');
      }
      //if string does not end with '.'
      if (
        !text.endsWith('.') &&
        !text.endsWith(';') &&
        !text.endsWith(',') &&
        !text.endsWith('?') &&
        !text.endsWith('!')
      ) {
        //add '.' to the end of string
        text = text + '.';
      }
      return text;
    }

    /*
     * Helper functions for describe()
     */

    //creates HTML structure for canvas descriptions
    fn._describeHTML = function(type, text) {
      const cnvId = this.canvas.id;
      if (type === 'fallback') {
        //if there is no description container
        if (!this.dummyDOM.querySelector(`#${cnvId + descContainer}`)) {
          //if there are no accessible outputs (see textOutput() and gridOutput())
          let html = `<div id="${cnvId}${descContainer}" role="region" aria-label="Canvas Description"><p id="${cnvId}${fallbackDescId}"></p></div>`;
          if (!this.dummyDOM.querySelector(`#${cnvId}accessibleOutput`)) {
            //create description container + <p> for fallback description
            this.dummyDOM.querySelector(`#${cnvId}`).innerHTML = html;
          } else {
            //create description container + <p> for fallback description before outputs
            this.dummyDOM
              .querySelector(`#${cnvId}accessibleOutput`)
              .insertAdjacentHTML('beforebegin', html);
          }
        } else {
          //if describeElement() has already created the container and added a table of elements
          //create fallback description <p> before the table
          this.dummyDOM
            .querySelector('#' + cnvId + fallbackTableId)
            .insertAdjacentHTML(
              'beforebegin',
              `<p id="${cnvId + fallbackDescId}"></p>`
            );
        }
        //if the container for the description exists
        this.descriptions.fallback = this.dummyDOM.querySelector(
          `#${cnvId}${fallbackDescId}`
        );
        this.descriptions.fallback.innerHTML = text;
        return;
      } else if (type === 'label') {
        //if there is no label container
        if (!this.dummyDOM.querySelector(`#${cnvId + labelContainer}`)) {
          let html = `<div id="${cnvId}${labelContainer}" class="p5Label"><p id="${cnvId}${labelDescId}"></p></div>`;
          //if there are no accessible outputs (see textOutput() and gridOutput())
          if (!this.dummyDOM.querySelector(`#${cnvId}accessibleOutputLabel`)) {
            //create label container + <p> for label description
            this.dummyDOM
              .querySelector('#' + cnvId)
              .insertAdjacentHTML('afterend', html);
          } else {
            //create label container + <p> for label description before outputs
            this.dummyDOM
              .querySelector(`#${cnvId}accessibleOutputLabel`)
              .insertAdjacentHTML('beforebegin', html);
          }
        } else if (this.dummyDOM.querySelector(`#${cnvId + labelTableId}`)) {
          //if describeElement() has already created the container and added a table of elements
          //create label description <p> before the table
          this.dummyDOM
            .querySelector(`#${cnvId + labelTableId}`)
            .insertAdjacentHTML(
              'beforebegin',
              `<p id="${cnvId}${labelDescId}"></p>`
            );
        }
        this.descriptions.label = this.dummyDOM.querySelector(
          '#' + cnvId + labelDescId
        );
        this.descriptions.label.innerHTML = text;
        return;
      }
    };

    /*
     * Helper functions for describeElement().
     */

    //check that name is not LABEL or FALLBACK and ensure text ends with colon
    function _elementName(name) {
      if (name === 'label' || name === 'fallback') {
        throw new Error('element name should not be LABEL or FALLBACK');
      }
      //check if last character of string n is '.', ';', or ','
      if (name.endsWith('.') || name.endsWith(';') || name.endsWith(',')) {
        //replace last character with ':'
        name = name.replace(/.$/, ':');
      } else if (!name.endsWith(':')) {
        //if string n does not end with ':'
        //add ':'' at the end of string
        name = name + ':';
      }
      return name;
    }

    //creates HTML structure for element descriptions
    fn._describeElementHTML = function(type, name, text) {
      const cnvId = this.canvas.id;
      if (type === 'fallback') {
        //if there is no description container
        if (!this.dummyDOM.querySelector(`#${cnvId + descContainer}`)) {
          //if there are no accessible outputs (see textOutput() and gridOutput())
          let html = `<div id="${cnvId}${descContainer}" role="region" aria-label="Canvas Description"><table id="${cnvId}${fallbackTableId}"><caption>Canvas elements and their descriptions</caption></table></div>`;
          if (!this.dummyDOM.querySelector(`#${cnvId}accessibleOutput`)) {
            //create container + table for element descriptions
            this.dummyDOM.querySelector('#' + cnvId).innerHTML = html;
          } else {
            //create container + table for element descriptions before outputs
            this.dummyDOM
              .querySelector(`#${cnvId}accessibleOutput`)
              .insertAdjacentHTML('beforebegin', html);
          }
        } else if (!this.dummyDOM.querySelector('#' + cnvId + fallbackTableId)) {
          //if describe() has already created the container and added a description
          //and there is no table create fallback table for element description after
          //fallback description
          this.dummyDOM
            .querySelector('#' + cnvId + fallbackDescId)
            .insertAdjacentHTML(
              'afterend',
              `<table id="${cnvId}${fallbackTableId}"><caption>Canvas elements and their descriptions</caption></table>`
            );
        }
        //create a table row for the element
        let tableRow = document.createElement('tr');
        tableRow.id = cnvId + fallbackTableElId + name;
        this.dummyDOM
          .querySelector('#' + cnvId + fallbackTableId)
          .appendChild(tableRow);
        //update element description
        this.descriptions.fallbackElements[name] = this.dummyDOM.querySelector(
          `#${cnvId}${fallbackTableElId}${name}`
        );
        this.descriptions.fallbackElements[name].innerHTML = text;
        return;
      } else if (type === 'label') {
        //If display is LABEL creates a div adjacent to the canvas element with
        //a table, a row header cell with the name of the elements,
        //and adds the description of the element in adjacent cell.
        //if there is no label description container
        if (!this.dummyDOM.querySelector(`#${cnvId + labelContainer}`)) {
          //if there are no accessible outputs (see textOutput() and gridOutput())
          let html = `<div id="${cnvId}${labelContainer}" class="p5Label"><table id="${cnvId}${labelTableId}"></table></div>`;
          if (!this.dummyDOM.querySelector(`#${cnvId}accessibleOutputLabel`)) {
            //create container + table for element descriptions
            this.dummyDOM
              .querySelector('#' + cnvId)
              .insertAdjacentHTML('afterend', html);
          } else {
            //create container + table for element descriptions before outputs
            this.dummyDOM
              .querySelector(`#${cnvId}accessibleOutputLabel`)
              .insertAdjacentHTML('beforebegin', html);
          }
        } else if (!this.dummyDOM.querySelector(`#${cnvId + labelTableId}`)) {
          //if describe() has already created the label container and added a description
          //and there is no table create label table for element description after
          //label description
          this.dummyDOM
            .querySelector('#' + cnvId + labelDescId)
            .insertAdjacentHTML(
              'afterend',
              `<table id="${cnvId + labelTableId}"></table>`
            );
        }
        //create a table row for the element label description
        let tableRow = document.createElement('tr');
        tableRow.id = cnvId + labelTableElId + name;
        this.dummyDOM
          .querySelector('#' + cnvId + labelTableId)
          .appendChild(tableRow);
        //update element label description
        this.descriptions.labelElements[name] = this.dummyDOM.querySelector(
          `#${cnvId}${labelTableElId}${name}`
        );
        this.descriptions.labelElements[name].innerHTML = text;
      }
    };
  }

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

  /**
   * @module Environment
   * @submodule Environment
   * @for p5
   * @requires core
   */

  function gridOutput(p5, fn){
    //the functions in this file support updating the grid output

    //updates gridOutput
    fn._updateGridOutput = function(idT) {
      if (this._renderer && this._renderer.isP3D) {
        if (!this._didOutputGridWebGLMessage) {
          this._didOutputGridWebGLMessage = true;
          console.error('gridOutput() does not yet work in WebGL mode.');
        }
        return;
      }
      //if html structure is not there yet
      if (!this.dummyDOM.querySelector(`#${idT}_summary`)) {
        return;
      }
      let current = this._accessibleOutputs[idT];
      //create shape details list
      let innerShapeDetails = _gridShapeDetails(idT, this.ingredients.shapes);
      //create summary
      let innerSummary = _gridSummary(
        innerShapeDetails.numShapes,
        this.ingredients.colors.background,
        this.width,
        this.height
      );
      //create grid map
      let innerMap = _gridMap(idT, this.ingredients.shapes);
      //if it is different from current summary
      if (innerSummary !== current.summary.innerHTML) {
        //update
        current.summary.innerHTML = innerSummary;
      }
      //if it is different from current map
      if (innerMap !== current.map.innerHTML) {
        //update
        current.map.innerHTML = innerMap;
      }
      //if it is different from current shape details
      if (innerShapeDetails.details !== current.shapeDetails.innerHTML) {
        //update
        current.shapeDetails.innerHTML = innerShapeDetails.details;
      }
      this._accessibleOutputs[idT] = current;
    };

    //creates spatial grid that maps the location of shapes
    function _gridMap(idT, ingredients) {
      let shapeNumber = 0;
      let table = '';
      //create an array of arrays 10*10 of empty cells
      let cells = Array.from(Array(10), () => Array(10));
      for (let x in ingredients) {
        for (let y in ingredients[x]) {
          let fill;
          if (x !== 'line') {
            fill = `<a href="#${idT}shape${shapeNumber}">${
            ingredients[x][y].color
          } ${x}</a>`;
          } else {
            fill = `<a href="#${idT}shape${shapeNumber}">${
            ingredients[x][y].color
          } ${x} midpoint</a>`;
          }

          // Check if shape is in canvas, skip if not
          if(
            ingredients[x][y].loc.locY < cells.length &&
            ingredients[x][y].loc.locX < cells[ingredients[x][y].loc.locY].length
          ){
            //if empty cell of location of shape is undefined
            if (!cells[ingredients[x][y].loc.locY][ingredients[x][y].loc.locX]) {
              //fill it with shape info
              cells[ingredients[x][y].loc.locY][ingredients[x][y].loc.locX] =
                fill;
              //if a shape is already in that location
            } else {
              //add it
              cells[ingredients[x][y].loc.locY][ingredients[x][y].loc.locX] =
                cells[ingredients[x][y].loc.locY][ingredients[x][y].loc.locX] +
                '  ' +
                fill;
            }
            shapeNumber++;
          }
        }
      }
      //make table based on array
      for (let _r in cells) {
        let row = '<tr>';
        for (let c in cells[_r]) {
          row = row + '<td>';
          if (cells[_r][c] !== undefined) {
            row = row + cells[_r][c];
          }
          row = row + '</td>';
        }
        table = table + row + '</tr>';
      }
      return table;
    }

    //creates grid summary
    function _gridSummary(numShapes, background, width, height) {
      let text = `${background} canvas, ${width} by ${height} pixels, contains ${
      numShapes[0]
    }`;
      if (numShapes[0] === 1) {
        text = `${text} shape: ${numShapes[1]}`;
      } else {
        text = `${text} shapes: ${numShapes[1]}`;
      }
      return text;
    }

    //creates list of shapes
    function _gridShapeDetails(idT, ingredients) {
      let shapeDetails = '';
      let shapes = '';
      let totalShapes = 0;
      //goes trhough every shape type in ingredients
      for (let x in ingredients) {
        let shapeNum = 0;
        for (let y in ingredients[x]) {
          //it creates a line in a list
          let line = `<li id="${idT}shape${totalShapes}">${
          ingredients[x][y].color
        } ${x},`;
          if (x === 'line') {
            line =
              line +
              ` location = ${ingredients[x][y].pos}, length = ${
              ingredients[x][y].length
            } pixels`;
          } else {
            line = line + ` location = ${ingredients[x][y].pos}`;
            if (x !== 'point') {
              line = line + `, area = ${ingredients[x][y].area} %`;
            }
            line = line + '</li>';
          }
          shapeDetails = shapeDetails + line;
          shapeNum++;
          totalShapes++;
        }
        if (shapeNum > 1) {
          shapes = `${shapes} ${shapeNum} ${x}s`;
        } else {
          shapes = `${shapes} ${shapeNum} ${x}`;
        }
      }
      return { numShapes: [totalShapes, shapes], details: shapeDetails };
    }
  }

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

  /**
   * @module Environment
   * @submodule Environment
   * @for p5
   * @requires core
   */

  function textOutput(p5, fn){
    //the functions in this file support updating the text output

    //updates textOutput
    fn._updateTextOutput = function(idT) {
      if (this._renderer && this._renderer.isP3D) {
        if (!this._didOutputTextWebGLMessage) {
          this._didOutputTextWebGLMessage = true;
          console.error('textOutput() does not yet work in WebGL mode.');
        }
        return;
      }
      //if html structure is not there yet
      if (!this.dummyDOM.querySelector(`#${idT}_summary`)) {
        return;
      }
      let current = this._accessibleOutputs[idT];
      //create shape list
      let innerList = _shapeList(idT, this.ingredients.shapes);
      //create output summary
      let innerSummary = _textSummary(
        innerList.numShapes,
        this.ingredients.colors.background,
        this.width,
        this.height
      );
      //create shape details
      let innerShapeDetails = _shapeDetails(idT, this.ingredients.shapes);
      //if it is different from current summary
      if (innerSummary !== current.summary.innerHTML) {
        //update
        current.summary.innerHTML = innerSummary;
      }
      //if it is different from current shape list
      if (innerList.listShapes !== current.list.innerHTML) {
        //update
        current.list.innerHTML = innerList.listShapes;
      }
      //if it is different from current shape details
      if (innerShapeDetails !== current.shapeDetails.innerHTML) {
        //update
        current.shapeDetails.innerHTML = innerShapeDetails;
      }
      this._accessibleOutputs[idT] = current;
    };

    //Builds textOutput summary
    function _textSummary(numShapes, background, width, height) {
      let text = `Your output is a, ${width} by ${height} pixels, ${background} canvas containing the following`;
      if (numShapes === 1) {
        text = `${text} shape:`;
      } else {
        text = `${text} ${numShapes} shapes:`;
      }
      return text;
    }

    //Builds textOutput table with shape details
    function _shapeDetails(idT, ingredients) {
      let shapeDetails = '';
      let shapeNumber = 0;
      //goes trhough every shape type in ingredients
      for (let x in ingredients) {
        //and for every shape
        for (let y in ingredients[x]) {
          //it creates a table row
          let row = `<tr id="${idT}shape${shapeNumber}"><th>${
          ingredients[x][y].color
        } ${x}</th>`;
          if (x === 'line') {
            row =
              row +
              `<td>location = ${ingredients[x][y].pos}</td><td>length = ${
              ingredients[x][y].length
            } pixels</td></tr>`;
          } else {
            row = row + `<td>location = ${ingredients[x][y].pos}</td>`;
            if (x !== 'point') {
              row = row + `<td> area = ${ingredients[x][y].area}%</td>`;
            }
            row = row + '</tr>';
          }
          shapeDetails = shapeDetails + row;
          shapeNumber++;
        }
      }
      return shapeDetails;
    }

    //Builds textOutput shape list
    function _shapeList(idT, ingredients) {
      let shapeList = '';
      let shapeNumber = 0;
      //goes trhough every shape type in ingredients
      for (let x in ingredients) {
        for (let y in ingredients[x]) {
          //it creates a line in a list
          let _line = `<li><a href="#${idT}shape${shapeNumber}">${
          ingredients[x][y].color
        } ${x}</a>`;
          if (x === 'line') {
            _line =
              _line +
              `, ${ingredients[x][y].pos}, ${
              ingredients[x][y].length
            } pixels long.</li>`;
          } else {
            _line = _line + `, at ${ingredients[x][y].pos}`;
            if (x !== 'point') {
              _line = _line + `, covering ${ingredients[x][y].area}% of the canvas`;
            }
            _line = _line + '.</li>';
          }
          shapeList = shapeList + _line;
          shapeNumber++;
        }
      }
      return { numShapes: shapeNumber, listShapes: shapeList };
    }
  }

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

  /**
   * @module Environment
   * @submodule Environment
   * @for p5
   * @requires core
   */

  function outputs(p5, fn){
    /**
     * Creates a screen reader-accessible description of shapes on the canvas.
     *
     * `textOutput()` adds a general description, list of shapes, and
     * table of shapes to the web page. The general description includes the
     * canvas size, canvas color, and number of shapes. For example,
     * `Your output is a, 100 by 100 pixels, gray canvas containing the following 2 shapes:`.
     *
     * A list of shapes follows the general description. The list describes the
     * color, location, and area of each shape. For example,
     * `a red circle at middle covering 3% of the canvas`. Each shape can be
     * selected to get more details.
     *
     * `textOutput()` uses its table of shapes as a list. The table describes the
     * shape, color, location, coordinates and area. For example,
     * `red circle location = middle area = 3%`. This is different from
     * <a href="#/p5/gridOutput">gridOutput()</a>, which uses its table as a grid.
     *
     * The `display` parameter is optional. It determines how the description is
     * displayed. If `LABEL` is passed, as in `textOutput(LABEL)`, the description
     * will be visible in a div element next to the canvas. Using `LABEL` creates
     * unhelpful duplicates for screen readers. Only use `LABEL` during
     * development. If `FALLBACK` is passed, as in `textOutput(FALLBACK)`, the
     * description will only be visible to screen readers. This is the default
     * mode.
     *
     * Read
     * <a href="/learn/accessible-labels.html">Writing accessible canvas descriptions</a>
     * to learn more about making sketches accessible.
     *
     * @method textOutput
     * @param  {(FALLBACK|LABEL)} [display] either FALLBACK or LABEL.
     *
     * @example
     * function setup() {
     *   // Add the text description.
     *   textOutput();
     *
     *   // Draw a couple of shapes.
     *   background(200);
     *   fill(255, 0, 0);
     *   circle(20, 20, 20);
     *   fill(0, 0, 255);
     *   square(50, 50, 50);
     *
     *   // Add a general description of the canvas.
     *   describe('A red circle and a blue square on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   // Add the text description and
     *   // display it for debugging.
     *   textOutput(LABEL);
     *
     *   // Draw a couple of shapes.
     *   background(200);
     *   fill(255, 0, 0);
     *   circle(20, 20, 20);
     *   fill(0, 0, 255);
     *   square(50, 50, 50);
     *
     *   // Add a general description of the canvas.
     *   describe('A red circle and a blue square on a gray background.');
     * }
     *
     * @example
     * function setup(){
     *  createCanvas(100, 100);
     * }
     *
     * function draw() {
     *   // Add the text description.
     *   textOutput();
     *
     *   // Draw a moving circle.
     *   background(200);
     *   let x = frameCount * 0.1;
     *   fill(255, 0, 0);
     *   circle(x, 20, 20);
     *   fill(0, 0, 255);
     *   square(50, 50, 50);
     *
     *   // Add a general description of the canvas.
     *   describe('A red circle moves from left to right above a blue square.');
     * }
     *
     * @example
     * function setup(){
     *  createCanvas(100, 100);
     * }
     *
     * function draw() {
     *   // Add the text description and
     *   // display it for debugging.
     *   textOutput(LABEL);
     *
     *   // Draw a moving circle.
     *   background(200);
     *   let x = frameCount * 0.1;
     *   fill(255, 0, 0);
     *   circle(x, 20, 20);
     *   fill(0, 0, 255);
     *   square(50, 50, 50);
     *
     *   // Add a general description of the canvas.
     *   describe('A red circle moves from left to right above a blue square.');
     * }
     */
    fn.textOutput = function(display) {
      // p5._validateParameters('textOutput', arguments);
      //if textOutput is already true
      if (this._accessibleOutputs.text) {
        return;
      } else {
        //make textOutput true
        this._accessibleOutputs.text = true;
        //create output for fallback
        this._createOutput('textOutput', 'Fallback');
        if (display === this.LABEL) {
          //make textOutput label true
          this._accessibleOutputs.textLabel = true;
          //create output for label
          this._createOutput('textOutput', 'Label');
        }
      }
    };

    /**
     * Creates a screen reader-accessible description of shapes on the canvas.
     *
     * `gridOutput()` adds a general description, table of shapes, and list of
     * shapes to the web page. The general description includes the canvas size,
     * canvas color, and number of shapes. For example,
     * `gray canvas, 100 by 100 pixels, contains 2 shapes:  1 circle 1 square`.
     *
     * `gridOutput()` uses its table of shapes as a grid. Each shape in the grid
     * is placed in a cell whose row and column correspond to the shape's location
     * on the canvas. The grid cells describe the color and type of shape at that
     * location. For example, `red circle`. These descriptions can be selected
     * individually to get more details. This is different from
     * <a href="#/p5/textOutput">textOutput()</a>, which uses its table as a list.
     *
     * A list of shapes follows the table. The list describes the color, type,
     * location, and area of each shape. For example,
     * `red circle, location = middle, area = 3 %`.
     *
     * The `display` parameter is optional. It determines how the description is
     * displayed. If `LABEL` is passed, as in `gridOutput(LABEL)`, the description
     * will be visible in a div element next to the canvas. Using `LABEL` creates
     * unhelpful duplicates for screen readers. Only use `LABEL` during
     * development. If `FALLBACK` is passed, as in `gridOutput(FALLBACK)`, the
     * description will only be visible to screen readers. This is the default
     * mode.
     *
     * Read
     * <a href="/learn/accessible-labels.html">Writing accessible canvas descriptions</a>
     * to learn more about making sketches accessible.
     *
     * @method gridOutput
     * @param  {(FALLBACK|LABEL)} [display] either FALLBACK or LABEL.
     *
     * @example
     * function setup() {
     *   // Add the grid description.
     *   gridOutput();
     *
     *   // Draw a couple of shapes.
     *   background(200);
     *   fill(255, 0, 0);
     *   circle(20, 20, 20);
     *   fill(0, 0, 255);
     *   square(50, 50, 50);
     *
     *   // Add a general description of the canvas.
     *   describe('A red circle and a blue square on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   // Add the grid description and
     *   // display it for debugging.
     *   gridOutput(LABEL);
     *
     *   // Draw a couple of shapes.
     *   background(200);
     *   fill(255, 0, 0);
     *   circle(20, 20, 20);
     *   fill(0, 0, 255);
     *   square(50, 50, 50);
     *
     *   // Add a general description of the canvas.
     *   describe('A red circle and a blue square on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     * }
     *
     * function draw() {
     *   // Add the grid description.
     *   gridOutput();
     *
     *   // Draw a moving circle.
     *   background(200);
     *   let x = frameCount * 0.1;
     *   fill(255, 0, 0);
     *   circle(x, 20, 20);
     *   fill(0, 0, 255);
     *   square(50, 50, 50);
     *
     *   // Add a general description of the canvas.
     *   describe('A red circle moves from left to right above a blue square.');
     * }
     *
     * @example
     * function setup(){
     *  createCanvas(100, 100);
     * }
     *
     * function draw() {
     *   // Add the grid description and
     *   // display it for debugging.
     *   gridOutput(LABEL);
     *
     *   // Draw a moving circle.
     *   background(200);
     *   let x = frameCount * 0.1;
     *   fill(255, 0, 0);
     *   circle(x, 20, 20);
     *   fill(0, 0, 255);
     *   square(50, 50, 50);
     *
     *   // Add a general description of the canvas.
     *   describe('A red circle moves from left to right above a blue square.');
     * }
     */
    fn.gridOutput = function(display) {
      // p5._validateParameters('gridOutput', arguments);
      //if gridOutput is already true
      if (this._accessibleOutputs.grid) {
        return;
      } else {
        //make gridOutput true
        this._accessibleOutputs.grid = true;
        //create output for fallback
        this._createOutput('gridOutput', 'Fallback');
        if (display === this.LABEL) {
          //make gridOutput label true
          this._accessibleOutputs.gridLabel = true;
          //create output for label
          this._createOutput('gridOutput', 'Label');
        }
      }
    };

    //helper function returns true when accessible outputs are true
    fn._addAccsOutput = function() {
      //if there are no accessible outputs create object with all false
      if (!this._accessibleOutputs) {
        this._accessibleOutputs = {
          text: false,
          grid: false,
          textLabel: false,
          gridLabel: false
        };
      }
      return this._accessibleOutputs.grid || this._accessibleOutputs.text;
    };

    //helper function that creates html structure for accessible outputs
    fn._createOutput = function(type, display) {
      let cnvId = this.canvas.id;
      //if there are no ingredients create object. this object stores data for the outputs
      if (!this.ingredients) {
        this.ingredients = {
          shapes: {},
          colors: { background: 'white', fill: 'white', stroke: 'black' },
          pShapes: '',
          pBackground: ''
        };
      }
      //if there is no dummyDOM create it
      if (!this.dummyDOM) {
        this.dummyDOM = document.getElementById(cnvId).parentNode;
      }
      let cIdT, container, inner;
      let query = '';
      if (display === 'Fallback') {
        cIdT = cnvId + type;
        container = cnvId + 'accessibleOutput';
        if (!this.dummyDOM.querySelector(`#${container}`)) {
          //if there is no canvas description (see describe() and describeElement())
          if (!this.dummyDOM.querySelector(`#${cnvId}_Description`)) {
            //create html structure inside of canvas
            this.dummyDOM.querySelector(
              `#${cnvId}`
            ).innerHTML = `<div id="${container}" role="region" aria-label="Canvas Outputs"></div>`;
          } else {
            //create html structure after canvas description container
            this.dummyDOM
              .querySelector(`#${cnvId}_Description`)
              .insertAdjacentHTML(
                'afterend',
                `<div id="${container}" role="region" aria-label="Canvas Outputs"></div>`
              );
          }
        }
      } else if (display === 'Label') {
        query = display;
        cIdT = cnvId + type + display;
        container = cnvId + 'accessibleOutput' + display;
        if (!this.dummyDOM.querySelector(`#${container}`)) {
          //if there is no canvas description label (see describe() and describeElement())
          if (!this.dummyDOM.querySelector(`#${cnvId}_Label`)) {
            //create html structure adjacent to canvas
            this.dummyDOM
              .querySelector(`#${cnvId}`)
              .insertAdjacentHTML('afterend', `<div id="${container}"></div>`);
          } else {
            //create html structure after canvas label
            this.dummyDOM
              .querySelector(`#${cnvId}_Label`)
              .insertAdjacentHTML('afterend', `<div id="${container}"></div>`);
          }
        }
      }
      //create an object to store the latest output. this object is used in _updateTextOutput() and _updateGridOutput()
      this._accessibleOutputs[cIdT] = {};
      if (type === 'textOutput') {
        query = `#${cnvId}gridOutput${query}`; //query is used to check if gridOutput already exists
        inner = `<div id="${cIdT}">Text Output<div id="${cIdT}Summary" aria-label="text output summary"><p id="${cIdT}_summary"></p><ul id="${cIdT}_list"></ul></div><table id="${cIdT}_shapeDetails" summary="text output shape details"></table></div>`;
        //if gridOutput already exists
        if (this.dummyDOM.querySelector(query)) {
          //create textOutput before gridOutput
          this.dummyDOM
            .querySelector(query)
            .insertAdjacentHTML('beforebegin', inner);
        } else {
          //create output inside of container
          this.dummyDOM.querySelector(`#${container}`).innerHTML = inner;
        }
        //store output html elements
        this._accessibleOutputs[cIdT].list = this.dummyDOM.querySelector(
          `#${cIdT}_list`
        );
      } else if (type === 'gridOutput') {
        query = `#${cnvId}textOutput${query}`; //query is used to check if textOutput already exists
        inner = `<div id="${cIdT}">Grid Output<p id="${cIdT}_summary" aria-label="grid output summary"><table id="${cIdT}_map" summary="grid output content"></table><ul id="${cIdT}_shapeDetails" aria-label="grid output shape details"></ul></div>`;
        //if textOutput already exists
        if (this.dummyDOM.querySelector(query)) {
          //create gridOutput after textOutput
          this.dummyDOM.querySelector(query).insertAdjacentHTML('afterend', inner);
        } else {
          //create output inside of container
          this.dummyDOM.querySelector(`#${container}`).innerHTML = inner;
        }
        //store output html elements
        this._accessibleOutputs[cIdT].map = this.dummyDOM.querySelector(
          `#${cIdT}_map`
        );
      }
      this._accessibleOutputs[cIdT].shapeDetails = this.dummyDOM.querySelector(
        `#${cIdT}_shapeDetails`
      );
      this._accessibleOutputs[cIdT].summary = this.dummyDOM.querySelector(
        `#${cIdT}_summary`
      );
    };

    //this function is called at the end of setup and draw if using
    //accessibleOutputs and calls update functions of outputs
    fn._updateAccsOutput = function() {
      let cnvId = this.canvas.id;
      //if the shapes are not the same as before
      if (
        JSON.stringify(this.ingredients.shapes) !== this.ingredients.pShapes ||
        this.ingredients.colors.background !== this.ingredients.pBackground
      ) {
        //save current shapes as string in pShapes
        this.ingredients.pShapes = JSON.stringify(this.ingredients.shapes);
        if (this._accessibleOutputs.text) {
          this._updateTextOutput(cnvId + 'textOutput');
        }
        if (this._accessibleOutputs.grid) {
          this._updateGridOutput(cnvId + 'gridOutput');
        }
        if (this._accessibleOutputs.textLabel) {
          this._updateTextOutput(cnvId + 'textOutputLabel');
        }
        if (this._accessibleOutputs.gridLabel) {
          this._updateGridOutput(cnvId + 'gridOutputLabel');
        }
      }
    };

    //helper function that resets all ingredients when background is called
    //and saves background color name
    fn._accsBackground = function(args) {
      //save current shapes as string in pShapes
      this.ingredients.pShapes = JSON.stringify(this.ingredients.shapes);
      this.ingredients.pBackground = this.ingredients.colors.background;
      //empty shapes JSON
      this.ingredients.shapes = {};
      //update background different
      if (this.ingredients.colors.backgroundRGBA !== args) {
        this.ingredients.colors.backgroundRGBA = args;
        this.ingredients.colors.background = this._rgbColorName(args);
      }
    };

    //helper function that gets fill and stroke of shapes
    fn._accsCanvasColors = function(f, args) {
      if (f === 'fill') {
        //update fill different
        if (this.ingredients.colors.fillRGBA !== args) {
          this.ingredients.colors.fillRGBA = args;
          this.ingredients.colors.fill = this._rgbColorName(args);
        }
      } else if (f === 'stroke') {
        //update stroke if different
        if (this.ingredients.colors.strokeRGBA !== args) {
          this.ingredients.colors.strokeRGBA = args;
          this.ingredients.colors.stroke = this._rgbColorName(args);
        }
      }
    };

    //builds ingredients.shapes used for building outputs
    fn._accsOutput = function(f, args) {
      if (f === 'ellipse' && args[2] === args[3]) {
        f = 'circle';
      } else if (f === 'rectangle' && args[2] === args[3]) {
        f = 'square';
      }
      let include = {};
      let add = true;
      let middle = _getMiddle(f, args);
      if (f === 'line') {
        //make color stroke
        include.color = this.ingredients.colors.stroke;
        //get lenght
        include.length = Math.round(
          Math.hypot(args[2] - args[0], args[3] - args[1])
        );
        //get position of end points
        let p1 = this._getPos(args[0], [1]);
        let p2 = this._getPos(args[2], [3]);
        include.loc = _canvasLocator(middle, this.width, this.height);
        if (p1 === p2) {
          include.pos = `at ${p1}`;
        } else {
          include.pos = `from ${p1} to ${p2}`;
        }
      } else {
        if (f === 'point') {
          //make color stroke
          include.color = this.ingredients.colors.stroke;
        } else {
          //make color fill
          include.color = this.ingredients.colors.fill;
          //get area of shape
          include.area = this._getArea(f, args);
        }
        //get middle of shapes
        //calculate position using middle of shape
        include.pos = this._getPos(...middle);
        //calculate location using middle of shape
        include.loc = _canvasLocator(middle, this.width, this.height);
      }
      //if it is the first time this shape is created
      if (!this.ingredients.shapes[f]) {
        this.ingredients.shapes[f] = [include];
        //if other shapes of this type have been created
      } else {
        //for every shape of this type
        for (let y in this.ingredients.shapes[f]) {
          //compare it with current shape and if it already exists make add false
          if (
            JSON.stringify(this.ingredients.shapes[f][y]) ===
            JSON.stringify(include)
          ) {
            add = false;
          }
        }
        //add shape by pushing it to the end
        if (add === true) {
          this.ingredients.shapes[f].push(include);
        }
      }
    };

    //gets middle point / centroid of shape
    function _getMiddle(f, args) {
      let x, y;
      if (
        f === 'rectangle' ||
        f === 'ellipse' ||
        f === 'arc' ||
        f === 'circle' ||
        f === 'square'
      ) {
        x = Math.round(args[0] + args[2] / 2);
        y = Math.round(args[1] + args[3] / 2);
      } else if (f === 'triangle') {
        x = (args[0] + args[2] + args[4]) / 3;
        y = (args[1] + args[3] + args[5]) / 3;
      } else if (f === 'quadrilateral') {
        x = (args[0] + args[2] + args[4] + args[6]) / 4;
        y = (args[1] + args[3] + args[5] + args[7]) / 4;
      } else if (f === 'line') {
        x = (args[0] + args[2]) / 2;
        y = (args[1] + args[3]) / 2;
      } else {
        x = args[0];
        y = args[1];
      }
      return [x, y];
    }

    //gets position of shape in the canvas
    fn._getPos = function (x, y) {
      const { x: transformedX, y: transformedY } =
        this.worldToScreen(new p5.Vector(x, y));
      const canvasWidth = this.width;
      const canvasHeight = this.height;
      if (transformedX < 0.4 * canvasWidth) {
        if (transformedY < 0.4 * canvasHeight) {
          return 'top left';
        } else if (transformedY > 0.6 * canvasHeight) {
          return 'bottom left';
        } else {
          return 'mid left';
        }
      } else if (transformedX > 0.6 * canvasWidth) {
        if (transformedY < 0.4 * canvasHeight) {
          return 'top right';
        } else if (transformedY > 0.6 * canvasHeight) {
          return 'bottom right';
        } else {
          return 'mid right';
        }
      } else {
        if (transformedY < 0.4 * canvasHeight) {
          return 'top middle';
        } else if (transformedY > 0.6 * canvasHeight) {
          return 'bottom middle';
        } else {
          return 'middle';
        }
      }
    };

    //locates shape in a 10*10 grid
    function _canvasLocator(args, canvasWidth, canvasHeight) {
      const noRows = 10;
      const noCols = 10;
      let locX = Math.floor(args[0] / canvasWidth * noRows);
      let locY = Math.floor(args[1] / canvasHeight * noCols);
      if (locX === noRows) {
        locX = locX - 1;
      }
      if (locY === noCols) {
        locY = locY - 1;
      }
      return {
        locX,
        locY
      };
    }

    //calculates area of shape
    fn._getArea = function (objectType, shapeArgs) {
      let objectArea = 0;
      if (objectType === 'arc') {
        // area of full ellipse = PI * horizontal radius * vertical radius.
        // therefore, area of arc = difference bet. arc's start and end radians * horizontal radius * vertical radius.
        // the below expression is adjusted for negative values and differences in arc's start and end radians over PI*2
        const arcSizeInRadians =
          ((shapeArgs[5] - shapeArgs[4]) % (Math.PI * 2) + Math.PI * 2) %
          (Math.PI * 2);
        objectArea = arcSizeInRadians * shapeArgs[2] * shapeArgs[3] / 8;
        if (shapeArgs[6] === 'open' || shapeArgs[6] === 'chord') {
          // when the arc's mode is OPEN or CHORD, we need to account for the area of the triangle that is formed to close the arc
          // (Ax( By −  Cy) + Bx(Cy − Ay) + Cx(Ay − By ) )/2
          const Ax = shapeArgs[0];
          const Ay = shapeArgs[1];
          const Bx =
            shapeArgs[0] + shapeArgs[2] / 2 * Math.cos(shapeArgs[4]).toFixed(2);
          const By =
            shapeArgs[1] + shapeArgs[3] / 2 * Math.sin(shapeArgs[4]).toFixed(2);
          const Cx =
            shapeArgs[0] + shapeArgs[2] / 2 * Math.cos(shapeArgs[5]).toFixed(2);
          const Cy =
            shapeArgs[1] + shapeArgs[3] / 2 * Math.sin(shapeArgs[5]).toFixed(2);
          const areaOfExtraTriangle =
            Math.abs(Ax * (By - Cy) + Bx * (Cy - Ay) + Cx * (Ay - By)) / 2;
          if (arcSizeInRadians > Math.PI) {
            objectArea = objectArea + areaOfExtraTriangle;
          } else {
            objectArea = objectArea - areaOfExtraTriangle;
          }
        }
      } else if (objectType === 'ellipse' || objectType === 'circle') {
        objectArea = 3.14 * shapeArgs[2] / 2 * shapeArgs[3] / 2;
      } else if (objectType === 'line') {
        objectArea = 0;
      } else if (objectType === 'point') {
        objectArea = 0;
      } else if (objectType === 'quadrilateral') {
        // ((x4+x1)*(y4-y1)+(x1+x2)*(y1-y2)+(x2+x3)*(y2-y3)+(x3+x4)*(y3-y4))/2
        objectArea =
          Math.abs(
            (shapeArgs[6] + shapeArgs[0]) * (shapeArgs[7] - shapeArgs[1]) +
              (shapeArgs[0] + shapeArgs[2]) * (shapeArgs[1] - shapeArgs[3]) +
              (shapeArgs[2] + shapeArgs[4]) * (shapeArgs[3] - shapeArgs[5]) +
              (shapeArgs[4] + shapeArgs[6]) * (shapeArgs[5] - shapeArgs[7])
          ) / 2;
      } else if (objectType === 'rectangle' || objectType === 'square') {
        objectArea = shapeArgs[2] * shapeArgs[3];
      } else if (objectType === 'triangle') {
        objectArea =
          Math.abs(
            shapeArgs[0] * (shapeArgs[3] - shapeArgs[5]) +
              shapeArgs[2] * (shapeArgs[5] - shapeArgs[1]) +
              shapeArgs[4] * (shapeArgs[1] - shapeArgs[3])
          ) / 2;
        // (Ax( By −  Cy) + Bx(Cy − Ay) + Cx(Ay − By ))/2
      }
      //  Store the positions of the canvas corners
      const canvasWidth = this.width * this._renderer._pixelDensity;
      const canvasHeight = this.height * this._renderer._pixelDensity;
      const canvasCorners = [
        new DOMPoint(0, 0),
        new DOMPoint(canvasWidth, 0),
        new DOMPoint(canvasWidth, canvasHeight),
        new DOMPoint(0, canvasHeight)
      ];
      //  Apply the inverse of the current transformations to the canvas corners
      const currentTransform = this._renderer.isP3D ?
        new DOMMatrix(this._renderer.uMVMatrix.mat4) :
        this.drawingContext.getTransform();
      const invertedTransform = currentTransform.inverse();
      const tc = canvasCorners.map(
        corner => corner.matrixTransform(invertedTransform)
      );
      /*  Use same shoelace formula used for quad area (above) to calculate
      the area of the canvas with inverted transformation applied */
      const transformedCanvasArea = Math.abs(
        (tc[3].x + tc[0].x) * (tc[3].y - tc[0].y) +
        (tc[0].x + tc[1].x) * (tc[0].y - tc[1].y) +
        (tc[1].x + tc[2].x) * (tc[1].y - tc[2].y)+
        (tc[2].x + tc[3].x) * (tc[2].y - tc[3].y)
      ) / 2;
      /*  Compare area of shape (minus transformations) to area of canvas
      with inverted transformation applied.
      Return percentage  */
      const untransformedArea = Math.round(
        objectArea * 100 / (transformedCanvasArea)
      );
      return untransformedArea;
    };
  }

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

  /**
   * @module Color
   * @submodule Color Conversion
   * @for p5
   * @requires core
   */

  p5$2.ColorConversion = {
    /**
     * Convert an HSBA array to HSLA.
     */
    _hsbaToHSLA(hsba) {
      const hue = hsba[0];
      let sat = hsba[1];
      const val = hsba[2];

      // Calculate lightness.
      const li = (2 - sat) * val / 2;

      // Convert saturation.
      if (li !== 0) {
        if (li === 1) {
          sat = 0;
        } else if (li < 0.5) {
          sat = sat / (2 - sat);
        } else {
          sat = sat * val / (2 - li * 2);
        }
      }

      // Hue and alpha stay the same.
      return [hue, sat, li, hsba[3]];
    },

    /**
     * Convert an HSBA array to RGBA.
     */
    _hsbaToRGBA(hsba) {
      const hue = hsba[0] * 6; // We will split hue into 6 sectors.
      const sat = hsba[1];
      const val = hsba[2];

      let RGBA = [];

      if (sat === 0) {
        RGBA = [val, val, val, hsba[3]]; // Return early if grayscale.
      } else {
        const sector = Math.floor(hue);
        const tint1 = val * (1 - sat);
        const tint2 = val * (1 - sat * (hue - sector));
        const tint3 = val * (1 - sat * (1 + sector - hue));
        let red, green, blue;
        if (sector === 1) {
          // Yellow to green.
          red = tint2;
          green = val;
          blue = tint1;
        } else if (sector === 2) {
          // Green to cyan.
          red = tint1;
          green = val;
          blue = tint3;
        } else if (sector === 3) {
          // Cyan to blue.
          red = tint1;
          green = tint2;
          blue = val;
        } else if (sector === 4) {
          // Blue to magenta.
          red = tint3;
          green = tint1;
          blue = val;
        } else if (sector === 5) {
          // Magenta to red.
          red = val;
          green = tint1;
          blue = tint2;
        } else {
          // Red to yellow (sector could be 0 or 6).
          red = val;
          green = tint3;
          blue = tint1;
        }
        RGBA = [red, green, blue, hsba[3]];
      }

      return RGBA;
    },

    /**
     * Convert an HSLA array to HSBA.
     */
    _hslaToHSBA(hsla) {
      const hue = hsla[0];
      let sat = hsla[1];
      const li = hsla[2];

      // Calculate brightness.
      let val;
      if (li < 0.5) {
        val = (1 + sat) * li;
      } else {
        val = li + sat - li * sat;
      }

      // Convert saturation.
      sat = 2 * (val - li) / val;

      // Hue and alpha stay the same.
      return [hue, sat, val, hsla[3]];
    },

    /**
     * Convert an HSLA array to RGBA.
     *
     * We need to change basis from HSLA to something that can be more easily be
     * projected onto RGBA. We will choose hue and brightness as our first two
     * components, and pick a convenient third one ('zest') so that we don't need
     * to calculate formal HSBA saturation.
     */
    _hslaToRGBA(hsla) {
      const hue = hsla[0] * 6; // We will split hue into 6 sectors.
      const sat = hsla[1];
      const li = hsla[2];

      let RGBA = [];

      if (sat === 0) {
        RGBA = [li, li, li, hsla[3]]; // Return early if grayscale.
      } else {
        // Calculate brightness.
        let val;
        if (li < 0.5) {
          val = (1 + sat) * li;
        } else {
          val = li + sat - li * sat;
        }

        // Define zest.
        const zest = 2 * li - val;

        // Implement projection (project onto green by default).
        const hzvToRGB = (hue, zest, val) => {
          if (hue < 0) {
            // Hue must wrap to allow projection onto red and blue.
            hue += 6;
          } else if (hue >= 6) {
            hue -= 6;
          }
          if (hue < 1) {
            // Red to yellow (increasing green).
            return zest + (val - zest) * hue;
          } else if (hue < 3) {
            // Yellow to cyan (greatest green).
            return val;
          } else if (hue < 4) {
            // Cyan to blue (decreasing green).
            return zest + (val - zest) * (4 - hue);
          } else {
            // Blue to red (least green).
            return zest;
          }
        };

        // Perform projections, offsetting hue as necessary.
        RGBA = [
          hzvToRGB(hue + 2, zest, val),
          hzvToRGB(hue, zest, val),
          hzvToRGB(hue - 2, zest, val),
          hsla[3]
        ];
      }

      return RGBA;
    },

    /**
     * Convert an RGBA array to HSBA.
     */
    _rgbaToHSBA(rgba) {
      const red = rgba[0];
      const green = rgba[1];
      const blue = rgba[2];

      const val = Math.max(red, green, blue);
      const chroma = val - Math.min(red, green, blue);

      let hue, sat;
      if (chroma === 0) {
        // Return early if grayscale.
        hue = 0;
        sat = 0;
      } else {
        sat = chroma / val;
        if (red === val) {
          // Magenta to yellow.
          hue = (green - blue) / chroma;
        } else if (green === val) {
          // Yellow to cyan.
          hue = 2 + (blue - red) / chroma;
        } else if (blue === val) {
          // Cyan to magenta.
          hue = 4 + (red - green) / chroma;
        }
        if (hue < 0) {
          // Confine hue to the interval [0, 1).
          hue += 6;
        } else if (hue >= 6) {
          hue -= 6;
        }
      }

      return [hue / 6, sat, val, rgba[3]];
    },

    /**
     * Convert an RGBA array to HSLA.
     */
    _rgbaToHSLA(rgba) {
      const red = rgba[0];
      const green = rgba[1];
      const blue = rgba[2];

      const val = Math.max(red, green, blue);
      const min = Math.min(red, green, blue);
      const li = val + min; // We will halve this later.
      const chroma = val - min;

      let hue, sat;
      if (chroma === 0) {
        // Return early if grayscale.
        hue = 0;
        sat = 0;
      } else {
        if (li < 1) {
          sat = chroma / li;
        } else {
          sat = chroma / (2 - li);
        }
        if (red === val) {
          // Magenta to yellow.
          hue = (green - blue) / chroma;
        } else if (green === val) {
          // Yellow to cyan.
          hue = 2 + (blue - red) / chroma;
        } else if (blue === val) {
          // Cyan to magenta.
          hue = 4 + (red - green) / chroma;
        }
        if (hue < 0) {
          // Confine hue to the interval [0, 1).
          hue += 6;
        } else if (hue >= 6) {
          hue -= 6;
        }
      }

      return [hue / 6, sat, li / 2, rgba[3]];
    }
  };
  var color_conversion = p5$2.ColorConversion;

  /**
   * @module Environment
   * @submodule Environment
   * @for p5
   * @requires core
   */


  function colorNamer(p5, fn){
    //stores the original hsb values
    let originalHSB;

    //stores values for color name exceptions
    const colorExceptions = [
      {
        h: 0,
        s: 0,
        b: 0.8275,
        name: 'gray'
      },
      {
        h: 0,
        s: 0,
        b: 0.8627,
        name: 'gray'
      },
      {
        h: 0,
        s: 0,
        b: 0.7529,
        name: 'gray'
      },
      {
        h: 0.0167,
        s: 0.1176,
        b: 1,
        name: 'light pink'
      }
    ];

    //stores values for color names
    const colorLookUp = [
      {
        h: 0,
        s: 0,
        b: 0,
        name: 'black'
      },
      {
        h: 0,
        s: 0,
        b: 0.5,
        name: 'gray'
      },
      {
        h: 0,
        s: 0,
        b: 1,
        name: 'white'
      },
      {
        h: 0,
        s: 0.5,
        b: 0.5,
        name: 'dark maroon'
      },
      {
        h: 0,
        s: 0.5,
        b: 1,
        name: 'salmon pink'
      },
      {
        h: 0,
        s: 1,
        b: 0,
        name: 'black'
      },
      {
        h: 0,
        s: 1,
        b: 0.5,
        name: 'dark red'
      },
      {
        h: 0,
        s: 1,
        b: 1,
        name: 'red'
      },
      {
        h: 5,
        s: 0,
        b: 1,
        name: 'very light peach'
      },
      {
        h: 5,
        s: 0.5,
        b: 0.5,
        name: 'brown'
      },
      {
        h: 5,
        s: 0.5,
        b: 1,
        name: 'peach'
      },
      {
        h: 5,
        s: 1,
        b: 0.5,
        name: 'brick red'
      },
      {
        h: 5,
        s: 1,
        b: 1,
        name: 'crimson'
      },
      {
        h: 10,
        s: 0,
        b: 1,
        name: 'light peach'
      },
      {
        h: 10,
        s: 0.5,
        b: 0.5,
        name: 'brown'
      },
      {
        h: 10,
        s: 0.5,
        b: 1,
        name: 'light orange'
      },
      {
        h: 10,
        s: 1,
        b: 0.5,
        name: 'brown'
      },
      {
        h: 10,
        s: 1,
        b: 1,
        name: 'orange'
      },
      {
        h: 15,
        s: 0,
        b: 1,
        name: 'very light yellow'
      },
      {
        h: 15,
        s: 0.5,
        b: 0.5,
        name: 'olive green'
      },
      {
        h: 15,
        s: 0.5,
        b: 1,
        name: 'light yellow'
      },
      {
        h: 15,
        s: 1,
        b: 0,
        name: 'dark olive green'
      },
      {
        h: 15,
        s: 1,
        b: 0.5,
        name: 'olive green'
      },
      {
        h: 15,
        s: 1,
        b: 1,
        name: 'yellow'
      },
      {
        h: 20,
        s: 0,
        b: 1,
        name: 'very light yellow'
      },
      {
        h: 20,
        s: 0.5,
        b: 0.5,
        name: 'olive green'
      },
      {
        h: 20,
        s: 0.5,
        b: 1,
        name: 'light yellow green'
      },
      {
        h: 20,
        s: 1,
        b: 0,
        name: 'dark olive green'
      },
      {
        h: 20,
        s: 1,
        b: 0.5,
        name: 'dark yellow green'
      },
      {
        h: 20,
        s: 1,
        b: 1,
        name: 'yellow green'
      },
      {
        h: 25,
        s: 0.5,
        b: 0.5,
        name: 'dark yellow green'
      },
      {
        h: 25,
        s: 0.5,
        b: 1,
        name: 'light green'
      },
      {
        h: 25,
        s: 1,
        b: 0.5,
        name: 'dark green'
      },
      {
        h: 25,
        s: 1,
        b: 1,
        name: 'green'
      },
      {
        h: 30,
        s: 0.5,
        b: 1,
        name: 'light green'
      },
      {
        h: 30,
        s: 1,
        b: 0.5,
        name: 'dark green'
      },
      {
        h: 30,
        s: 1,
        b: 1,
        name: 'green'
      },
      {
        h: 35,
        s: 0,
        b: 0.5,
        name: 'light green'
      },
      {
        h: 35,
        s: 0,
        b: 1,
        name: 'very light green'
      },
      {
        h: 35,
        s: 0.5,
        b: 0.5,
        name: 'dark green'
      },
      {
        h: 35,
        s: 0.5,
        b: 1,
        name: 'light green'
      },
      {
        h: 35,
        s: 1,
        b: 0,
        name: 'very dark green'
      },
      {
        h: 35,
        s: 1,
        b: 0.5,
        name: 'dark green'
      },
      {
        h: 35,
        s: 1,
        b: 1,
        name: 'green'
      },
      {
        h: 40,
        s: 0,
        b: 1,
        name: 'very light green'
      },
      {
        h: 40,
        s: 0.5,
        b: 0.5,
        name: 'dark green'
      },
      {
        h: 40,
        s: 0.5,
        b: 1,
        name: 'light green'
      },
      {
        h: 40,
        s: 1,
        b: 0.5,
        name: 'dark green'
      },
      {
        h: 40,
        s: 1,
        b: 1,
        name: 'green'
      },
      {
        h: 45,
        s: 0.5,
        b: 1,
        name: 'light turquoise'
      },
      {
        h: 45,
        s: 1,
        b: 0.5,
        name: 'dark turquoise'
      },
      {
        h: 45,
        s: 1,
        b: 1,
        name: 'turquoise'
      },
      {
        h: 50,
        s: 0,
        b: 1,
        name: 'light sky blue'
      },
      {
        h: 50,
        s: 0.5,
        b: 0.5,
        name: 'dark cyan'
      },
      {
        h: 50,
        s: 0.5,
        b: 1,
        name: 'light cyan'
      },
      {
        h: 50,
        s: 1,
        b: 0.5,
        name: 'dark cyan'
      },
      {
        h: 50,
        s: 1,
        b: 1,
        name: 'cyan'
      },
      {
        h: 55,
        s: 0,
        b: 1,
        name: 'light sky blue'
      },
      {
        h: 55,
        s: 0.5,
        b: 1,
        name: 'light sky blue'
      },
      {
        h: 55,
        s: 1,
        b: 0.5,
        name: 'dark blue'
      },
      {
        h: 55,
        s: 1,
        b: 1,
        name: 'sky blue'
      },
      {
        h: 60,
        s: 0,
        b: 0.5,
        name: 'gray'
      },
      {
        h: 60,
        s: 0,
        b: 1,
        name: 'very light blue'
      },
      {
        h: 60,
        s: 0.5,
        b: 0.5,
        name: 'blue'
      },
      {
        h: 60,
        s: 0.5,
        b: 1,
        name: 'light blue'
      },
      {
        h: 60,
        s: 1,
        b: 0.5,
        name: 'navy blue'
      },
      {
        h: 60,
        s: 1,
        b: 1,
        name: 'blue'
      },
      {
        h: 65,
        s: 0,
        b: 1,
        name: 'lavender'
      },
      {
        h: 65,
        s: 0.5,
        b: 0.5,
        name: 'navy blue'
      },
      {
        h: 65,
        s: 0.5,
        b: 1,
        name: 'light purple'
      },
      {
        h: 65,
        s: 1,
        b: 0.5,
        name: 'dark navy blue'
      },
      {
        h: 65,
        s: 1,
        b: 1,
        name: 'blue'
      },
      {
        h: 70,
        s: 0,
        b: 1,
        name: 'lavender'
      },
      {
        h: 70,
        s: 0.5,
        b: 0.5,
        name: 'navy blue'
      },
      {
        h: 70,
        s: 0.5,
        b: 1,
        name: 'lavender blue'
      },
      {
        h: 70,
        s: 1,
        b: 0.5,
        name: 'dark navy blue'
      },
      {
        h: 70,
        s: 1,
        b: 1,
        name: 'blue'
      },
      {
        h: 75,
        s: 0.5,
        b: 1,
        name: 'lavender'
      },
      {
        h: 75,
        s: 1,
        b: 0.5,
        name: 'dark purple'
      },
      {
        h: 75,
        s: 1,
        b: 1,
        name: 'purple'
      },
      {
        h: 80,
        s: 0.5,
        b: 1,
        name: 'pinkish purple'
      },
      {
        h: 80,
        s: 1,
        b: 0.5,
        name: 'dark purple'
      },
      {
        h: 80,
        s: 1,
        b: 1,
        name: 'purple'
      },
      {
        h: 85,
        s: 0,
        b: 1,
        name: 'light pink'
      },
      {
        h: 85,
        s: 0.5,
        b: 0.5,
        name: 'purple'
      },
      {
        h: 85,
        s: 0.5,
        b: 1,
        name: 'light fuchsia'
      },
      {
        h: 85,
        s: 1,
        b: 0.5,
        name: 'dark fuchsia'
      },
      {
        h: 85,
        s: 1,
        b: 1,
        name: 'fuchsia'
      },
      {
        h: 90,
        s: 0.5,
        b: 0.5,
        name: 'dark fuchsia'
      },
      {
        h: 90,
        s: 0.5,
        b: 1,
        name: 'hot pink'
      },
      {
        h: 90,
        s: 1,
        b: 0.5,
        name: 'dark fuchsia'
      },
      {
        h: 90,
        s: 1,
        b: 1,
        name: 'fuchsia'
      },
      {
        h: 95,
        s: 0,
        b: 1,
        name: 'pink'
      },
      {
        h: 95,
        s: 0.5,
        b: 1,
        name: 'light pink'
      },
      {
        h: 95,
        s: 1,
        b: 0.5,
        name: 'dark magenta'
      },
      {
        h: 95,
        s: 1,
        b: 1,
        name: 'magenta'
      }
    ];

    //returns text with color name
    function _calculateColor(hsb) {
      let colortext;
      //round hue
      if (hsb[0] !== 0) {
        hsb[0] = Math.round(hsb[0] * 100);
        let hue = hsb[0].toString().split('');
        const last = hue.length - 1;
        hue[last] = parseInt(hue[last]);
        //if last digit of hue is < 2.5 make it 0
        if (hue[last] < 2.5) {
          hue[last] = 0;
          //if last digit of hue is >= 2.5 and less than 7.5 make it 5
        } else if (hue[last] >= 2.5 && hue[last] < 7.5) {
          hue[last] = 5;
        }
        //if hue only has two digits
        if (hue.length === 2) {
          hue[0] = parseInt(hue[0]);
          //if last is greater than 7.5
          if (hue[last] >= 7.5) {
            //add one to the tens
            hue[last] = 0;
            hue[0] = hue[0] + 1;
          }
          hsb[0] = hue[0] * 10 + hue[1];
        } else {
          if (hue[last] >= 7.5) {
            hsb[0] = 10;
          } else {
            hsb[0] = hue[last];
          }
        }
      }
      //map brightness from 0 to 1
      hsb[2] = hsb[2] / 255;
      //round saturation and brightness
      for (let i = hsb.length - 1; i >= 1; i--) {
        if (hsb[i] <= 0.25) {
          hsb[i] = 0;
        } else if (hsb[i] > 0.25 && hsb[i] < 0.75) {
          hsb[i] = 0.5;
        } else {
          hsb[i] = 1;
        }
      }
      //after rounding, if the values are hue 0, saturation 0 and brightness 1
      //look at color exceptions which includes several tones from white to gray
      if (hsb[0] === 0 && hsb[1] === 0 && hsb[2] === 1) {
        //round original hsb values
        for (let i = 2; i >= 0; i--) {
          originalHSB[i] = Math.round(originalHSB[i] * 10000) / 10000;
        }
        //compare with the values in the colorExceptions array
        for (let e = 0; e < colorExceptions.length; e++) {
          if (
            colorExceptions[e].h === originalHSB[0] &&
            colorExceptions[e].s === originalHSB[1] &&
            colorExceptions[e].b === originalHSB[2]
          ) {
            colortext = colorExceptions[e].name;
            break;
          } else {
            //if there is no match return white
            colortext = 'white';
          }
        }
      } else {
        //otherwise, compare with values in colorLookUp
        for (let i = 0; i < colorLookUp.length; i++) {
          if (
            colorLookUp[i].h === hsb[0] &&
            colorLookUp[i].s === hsb[1] &&
            colorLookUp[i].b === hsb[2]
          ) {
            colortext = colorLookUp[i].name;
            break;
          }
        }
      }
      return colortext;
    }

    //gets rgba and returs a color name
    fn._rgbColorName = function(arg) {
      //conversts rgba to hsb
      let hsb = color_conversion._rgbaToHSBA(arg);
      //stores hsb in global variable
      originalHSB = hsb;
      //calculate color name
      return _calculateColor([hsb[0], hsb[1], hsb[2]]);
    };
  }

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

  function accessibility(p5){
    p5.registerAddon(describe);
    p5.registerAddon(gridOutput);
    p5.registerAddon(textOutput);
    p5.registerAddon(outputs);
    p5.registerAddon(colorNamer);
  }

  function color(p5){
    p5.registerAddon(creatingReading);
    p5.registerAddon(color$1);
    p5.registerAddon(setting);
  }

  function _typeof(o) {
    "@babel/helpers - typeof";

    return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
      return typeof o;
    } : function (o) {
      return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
    }, _typeof(o);
  }

  function toPrimitive(t, r) {
    if ("object" != _typeof(t) || !t) return t;
    var e = t[Symbol.toPrimitive];
    if (void 0 !== e) {
      var i = e.call(t, r);
      if ("object" != _typeof(i)) return i;
      throw new TypeError("@@toPrimitive must return a primitive value.");
    }
    return (String )(t);
  }

  function toPropertyKey(t) {
    var i = toPrimitive(t, "string");
    return "symbol" == _typeof(i) ? i : i + "";
  }

  function _defineProperty(e, r, t) {
    return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
      value: t,
      enumerable: true,
      configurable: true,
      writable: true
    }) : e[r] = t, e;
  }

  function _objectSpread(e) {
    for (var r = 1; r < arguments.length; r++) {
      var t = null != arguments[r] ? Object(arguments[r]) : {},
        o = Object.keys(t);
      "function" == typeof Object.getOwnPropertySymbols && o.push.apply(o, Object.getOwnPropertySymbols(t).filter(function (e) {
        return Object.getOwnPropertyDescriptor(t, e).enumerable;
      })), o.forEach(function (r) {
        _defineProperty(e, r, t[r]);
      });
    }
    return e;
  }

  function _classCallCheck(a, n) {
    if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function");
  }

  function _defineProperties(e, r) {
    for (var t = 0; t < r.length; t++) {
      var o = r[t];
      o.enumerable = o.enumerable || false, o.configurable = true, "value" in o && (o.writable = true), Object.defineProperty(e, toPropertyKey(o.key), o);
    }
  }
  function _createClass(e, r, t) {
    return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", {
      writable: false
    }), e;
  }

  function _assertThisInitialized(e) {
    if (void 0 === e) throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
    return e;
  }

  function _possibleConstructorReturn(t, e) {
    if (e && ("object" == _typeof(e) || "function" == typeof e)) return e;
    if (void 0 !== e) throw new TypeError("Derived constructors may only return object or undefined");
    return _assertThisInitialized(t);
  }

  function _getPrototypeOf(t) {
    return _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function (t) {
      return t.__proto__ || Object.getPrototypeOf(t);
    }, _getPrototypeOf(t);
  }

  function _setPrototypeOf(t, e) {
    return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) {
      return t.__proto__ = e, t;
    }, _setPrototypeOf(t, e);
  }

  function _inherits(t, e) {
    if ("function" != typeof e && null !== e) throw new TypeError("Super expression must either be null or a function");
    t.prototype = Object.create(e && e.prototype, {
      constructor: {
        value: t,
        writable: true,
        configurable: true
      }
    }), Object.defineProperty(t, "prototype", {
      writable: false
    }), e && _setPrototypeOf(t, e);
  }

  var consoleLogger = {
    type: 'logger',
    log: function log(args) {
      this.output('log', args);
    },
    warn: function warn(args) {
      this.output('warn', args);
    },
    error: function error(args) {
      this.output('error', args);
    },
    output: function output(type, args) {
      if (console && console[type]) console[type].apply(console, args);
    }
  };

  var Logger = function () {
    function Logger(concreteLogger) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

      _classCallCheck(this, Logger);

      this.init(concreteLogger, options);
    }

    _createClass(Logger, [{
      key: "init",
      value: function init(concreteLogger) {
        var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
        this.prefix = options.prefix || 'i18next:';
        this.logger = concreteLogger || consoleLogger;
        this.options = options;
        this.debug = options.debug;
      }
    }, {
      key: "setDebug",
      value: function setDebug(bool) {
        this.debug = bool;
      }
    }, {
      key: "log",
      value: function log() {
        for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
          args[_key] = arguments[_key];
        }

        return this.forward(args, 'log', '', true);
      }
    }, {
      key: "warn",
      value: function warn() {
        for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
          args[_key2] = arguments[_key2];
        }

        return this.forward(args, 'warn', '', true);
      }
    }, {
      key: "error",
      value: function error() {
        for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
          args[_key3] = arguments[_key3];
        }

        return this.forward(args, 'error', '');
      }
    }, {
      key: "deprecate",
      value: function deprecate() {
        for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
          args[_key4] = arguments[_key4];
        }

        return this.forward(args, 'warn', 'WARNING DEPRECATED: ', true);
      }
    }, {
      key: "forward",
      value: function forward(args, lvl, prefix, debugOnly) {
        if (debugOnly && !this.debug) return null;
        if (typeof args[0] === 'string') args[0] = "".concat(prefix).concat(this.prefix, " ").concat(args[0]);
        return this.logger[lvl](args);
      }
    }, {
      key: "create",
      value: function create(moduleName) {
        return new Logger(this.logger, _objectSpread({}, {
          prefix: "".concat(this.prefix, ":").concat(moduleName, ":")
        }, this.options));
      }
    }]);

    return Logger;
  }();

  var baseLogger = new Logger();

  var EventEmitter = function () {
    function EventEmitter() {
      _classCallCheck(this, EventEmitter);

      this.observers = {};
    }

    _createClass(EventEmitter, [{
      key: "on",
      value: function on(events, listener) {
        var _this = this;

        events.split(' ').forEach(function (event) {
          _this.observers[event] = _this.observers[event] || [];

          _this.observers[event].push(listener);
        });
        return this;
      }
    }, {
      key: "off",
      value: function off(event, listener) {
        if (!this.observers[event]) return;

        if (!listener) {
          delete this.observers[event];
          return;
        }

        this.observers[event] = this.observers[event].filter(function (l) {
          return l !== listener;
        });
      }
    }, {
      key: "emit",
      value: function emit(event) {
        for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
          args[_key - 1] = arguments[_key];
        }

        if (this.observers[event]) {
          var cloned = [].concat(this.observers[event]);
          cloned.forEach(function (observer) {
            observer.apply(void 0, args);
          });
        }

        if (this.observers['*']) {
          var _cloned = [].concat(this.observers['*']);

          _cloned.forEach(function (observer) {
            observer.apply(observer, [event].concat(args));
          });
        }
      }
    }]);

    return EventEmitter;
  }();

  function defer() {
    var res;
    var rej;
    var promise = new Promise(function (resolve, reject) {
      res = resolve;
      rej = reject;
    });
    promise.resolve = res;
    promise.reject = rej;
    return promise;
  }
  function makeString(object) {
    if (object == null) return '';
    return '' + object;
  }
  function copy(a, s, t) {
    a.forEach(function (m) {
      if (s[m]) t[m] = s[m];
    });
  }

  function getLastOfPath(object, path, Empty) {
    function cleanKey(key) {
      return key && key.indexOf('###') > -1 ? key.replace(/###/g, '.') : key;
    }

    function canNotTraverseDeeper() {
      return !object || typeof object === 'string';
    }

    var stack = typeof path !== 'string' ? [].concat(path) : path.split('.');

    while (stack.length > 1) {
      if (canNotTraverseDeeper()) return {};
      var key = cleanKey(stack.shift());
      if (!object[key] && Empty) object[key] = new Empty();

      if (Object.prototype.hasOwnProperty.call(object, key)) {
        object = object[key];
      } else {
        object = {};
      }
    }

    if (canNotTraverseDeeper()) return {};
    return {
      obj: object,
      k: cleanKey(stack.shift())
    };
  }

  function setPath(object, path, newValue) {
    var _getLastOfPath = getLastOfPath(object, path, Object),
        obj = _getLastOfPath.obj,
        k = _getLastOfPath.k;

    obj[k] = newValue;
  }
  function pushPath(object, path, newValue, concat) {
    var _getLastOfPath2 = getLastOfPath(object, path, Object),
        obj = _getLastOfPath2.obj,
        k = _getLastOfPath2.k;

    obj[k] = obj[k] || [];
    obj[k].push(newValue);
  }
  function getPath(object, path) {
    var _getLastOfPath3 = getLastOfPath(object, path),
        obj = _getLastOfPath3.obj,
        k = _getLastOfPath3.k;

    if (!obj) return undefined;
    return obj[k];
  }
  function getPathWithDefaults(data, defaultData, key) {
    var value = getPath(data, key);

    if (value !== undefined) {
      return value;
    }

    return getPath(defaultData, key);
  }
  function deepExtend(target, source, overwrite) {
    for (var prop in source) {
      if (prop !== '__proto__' && prop !== 'constructor') {
        if (prop in target) {
          if (typeof target[prop] === 'string' || target[prop] instanceof String || typeof source[prop] === 'string' || source[prop] instanceof String) {
            if (overwrite) target[prop] = source[prop];
          } else {
            deepExtend(target[prop], source[prop], overwrite);
          }
        } else {
          target[prop] = source[prop];
        }
      }
    }

    return target;
  }
  function regexEscape(str) {
    return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
  }
  var _entityMap = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;',
    '/': '&#x2F;'
  };
  function escape(data) {
    if (typeof data === 'string') {
      return data.replace(/[&<>"'\/]/g, function (s) {
        return _entityMap[s];
      });
    }

    return data;
  }
  var isIE10 = typeof window !== 'undefined' && window.navigator && window.navigator.userAgent && window.navigator.userAgent.indexOf('MSIE') > -1;

  var ResourceStore = function (_EventEmitter) {
    _inherits(ResourceStore, _EventEmitter);

    function ResourceStore(data) {
      var _this;

      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
        ns: ['translation'],
        defaultNS: 'translation'
      };

      _classCallCheck(this, ResourceStore);

      _this = _possibleConstructorReturn(this, _getPrototypeOf(ResourceStore).call(this));

      if (isIE10) {
        EventEmitter.call(_assertThisInitialized(_this));
      }

      _this.data = data || {};
      _this.options = options;

      if (_this.options.keySeparator === undefined) {
        _this.options.keySeparator = '.';
      }

      return _this;
    }

    _createClass(ResourceStore, [{
      key: "addNamespaces",
      value: function addNamespaces(ns) {
        if (this.options.ns.indexOf(ns) < 0) {
          this.options.ns.push(ns);
        }
      }
    }, {
      key: "removeNamespaces",
      value: function removeNamespaces(ns) {
        var index = this.options.ns.indexOf(ns);

        if (index > -1) {
          this.options.ns.splice(index, 1);
        }
      }
    }, {
      key: "getResource",
      value: function getResource(lng, ns, key) {
        var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
        var keySeparator = options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator;
        var path = [lng, ns];
        if (key && typeof key !== 'string') path = path.concat(key);
        if (key && typeof key === 'string') path = path.concat(keySeparator ? key.split(keySeparator) : key);

        if (lng.indexOf('.') > -1) {
          path = lng.split('.');
        }

        return getPath(this.data, path);
      }
    }, {
      key: "addResource",
      value: function addResource(lng, ns, key, value) {
        var options = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {
          silent: false
        };
        var keySeparator = this.options.keySeparator;
        if (keySeparator === undefined) keySeparator = '.';
        var path = [lng, ns];
        if (key) path = path.concat(keySeparator ? key.split(keySeparator) : key);

        if (lng.indexOf('.') > -1) {
          path = lng.split('.');
          value = ns;
          ns = path[1];
        }

        this.addNamespaces(ns);
        setPath(this.data, path, value);
        if (!options.silent) this.emit('added', lng, ns, key, value);
      }
    }, {
      key: "addResources",
      value: function addResources(lng, ns, resources) {
        var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {
          silent: false
        };

        for (var m in resources) {
          if (typeof resources[m] === 'string' || Object.prototype.toString.apply(resources[m]) === '[object Array]') this.addResource(lng, ns, m, resources[m], {
            silent: true
          });
        }

        if (!options.silent) this.emit('added', lng, ns, resources);
      }
    }, {
      key: "addResourceBundle",
      value: function addResourceBundle(lng, ns, resources, deep, overwrite) {
        var options = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : {
          silent: false
        };
        var path = [lng, ns];

        if (lng.indexOf('.') > -1) {
          path = lng.split('.');
          deep = resources;
          resources = ns;
          ns = path[1];
        }

        this.addNamespaces(ns);
        var pack = getPath(this.data, path) || {};

        if (deep) {
          deepExtend(pack, resources, overwrite);
        } else {
          pack = _objectSpread({}, pack, resources);
        }

        setPath(this.data, path, pack);
        if (!options.silent) this.emit('added', lng, ns, resources);
      }
    }, {
      key: "removeResourceBundle",
      value: function removeResourceBundle(lng, ns) {
        if (this.hasResourceBundle(lng, ns)) {
          delete this.data[lng][ns];
        }

        this.removeNamespaces(ns);
        this.emit('removed', lng, ns);
      }
    }, {
      key: "hasResourceBundle",
      value: function hasResourceBundle(lng, ns) {
        return this.getResource(lng, ns) !== undefined;
      }
    }, {
      key: "getResourceBundle",
      value: function getResourceBundle(lng, ns) {
        if (!ns) ns = this.options.defaultNS;
        if (this.options.compatibilityAPI === 'v1') return _objectSpread({}, {}, this.getResource(lng, ns));
        return this.getResource(lng, ns);
      }
    }, {
      key: "getDataByLanguage",
      value: function getDataByLanguage(lng) {
        return this.data[lng];
      }
    }, {
      key: "toJSON",
      value: function toJSON() {
        return this.data;
      }
    }]);

    return ResourceStore;
  }(EventEmitter);

  var postProcessor = {
    processors: {},
    addPostProcessor: function addPostProcessor(module) {
      this.processors[module.name] = module;
    },
    handle: function handle(processors, value, key, options, translator) {
      var _this = this;

      processors.forEach(function (processor) {
        if (_this.processors[processor]) value = _this.processors[processor].process(value, key, options, translator);
      });
      return value;
    }
  };

  var checkedLoadedFor = {};

  var Translator = function (_EventEmitter) {
    _inherits(Translator, _EventEmitter);

    function Translator(services) {
      var _this;

      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

      _classCallCheck(this, Translator);

      _this = _possibleConstructorReturn(this, _getPrototypeOf(Translator).call(this));

      if (isIE10) {
        EventEmitter.call(_assertThisInitialized(_this));
      }

      copy(['resourceStore', 'languageUtils', 'pluralResolver', 'interpolator', 'backendConnector', 'i18nFormat', 'utils'], services, _assertThisInitialized(_this));
      _this.options = options;

      if (_this.options.keySeparator === undefined) {
        _this.options.keySeparator = '.';
      }

      _this.logger = baseLogger.create('translator');
      return _this;
    }

    _createClass(Translator, [{
      key: "changeLanguage",
      value: function changeLanguage(lng) {
        if (lng) this.language = lng;
      }
    }, {
      key: "exists",
      value: function exists(key) {
        var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
          interpolation: {}
        };
        var resolved = this.resolve(key, options);
        return resolved && resolved.res !== undefined;
      }
    }, {
      key: "extractFromKey",
      value: function extractFromKey(key, options) {
        var nsSeparator = options.nsSeparator !== undefined ? options.nsSeparator : this.options.nsSeparator;
        if (nsSeparator === undefined) nsSeparator = ':';
        var keySeparator = options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator;
        var namespaces = options.ns || this.options.defaultNS;

        if (nsSeparator && key.indexOf(nsSeparator) > -1) {
          var m = key.match(this.interpolator.nestingRegexp);

          if (m && m.length > 0) {
            return {
              key: key,
              namespaces: namespaces
            };
          }

          var parts = key.split(nsSeparator);
          if (nsSeparator !== keySeparator || nsSeparator === keySeparator && this.options.ns.indexOf(parts[0]) > -1) namespaces = parts.shift();
          key = parts.join(keySeparator);
        }

        if (typeof namespaces === 'string') namespaces = [namespaces];
        return {
          key: key,
          namespaces: namespaces
        };
      }
    }, {
      key: "translate",
      value: function translate(keys, options, lastKey) {
        var _this2 = this;

        if (_typeof(options) !== 'object' && this.options.overloadTranslationOptionHandler) {
          options = this.options.overloadTranslationOptionHandler(arguments);
        }

        if (!options) options = {};
        if (keys === undefined || keys === null) return '';
        if (!Array.isArray(keys)) keys = [String(keys)];
        var keySeparator = options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator;

        var _this$extractFromKey = this.extractFromKey(keys[keys.length - 1], options),
            key = _this$extractFromKey.key,
            namespaces = _this$extractFromKey.namespaces;

        var namespace = namespaces[namespaces.length - 1];
        var lng = options.lng || this.language;
        var appendNamespaceToCIMode = options.appendNamespaceToCIMode || this.options.appendNamespaceToCIMode;

        if (lng && lng.toLowerCase() === 'cimode') {
          if (appendNamespaceToCIMode) {
            var nsSeparator = options.nsSeparator || this.options.nsSeparator;
            return namespace + nsSeparator + key;
          }

          return key;
        }

        var resolved = this.resolve(keys, options);
        var res = resolved && resolved.res;
        var resUsedKey = resolved && resolved.usedKey || key;
        var resExactUsedKey = resolved && resolved.exactUsedKey || key;
        var resType = Object.prototype.toString.apply(res);
        var noObject = ['[object Number]', '[object Function]', '[object RegExp]'];
        var joinArrays = options.joinArrays !== undefined ? options.joinArrays : this.options.joinArrays;
        var handleAsObjectInI18nFormat = !this.i18nFormat || this.i18nFormat.handleAsObject;
        var handleAsObject = typeof res !== 'string' && typeof res !== 'boolean' && typeof res !== 'number';

        if (handleAsObjectInI18nFormat && res && handleAsObject && noObject.indexOf(resType) < 0 && !(typeof joinArrays === 'string' && resType === '[object Array]')) {
          if (!options.returnObjects && !this.options.returnObjects) {
            this.logger.warn('accessing an object - but returnObjects options is not enabled!');
            return this.options.returnedObjectHandler ? this.options.returnedObjectHandler(resUsedKey, res, options) : "key '".concat(key, " (").concat(this.language, ")' returned an object instead of string.");
          }

          if (keySeparator) {
            var resTypeIsArray = resType === '[object Array]';
            var copy = resTypeIsArray ? [] : {};
            var newKeyToUse = resTypeIsArray ? resExactUsedKey : resUsedKey;

            for (var m in res) {
              if (Object.prototype.hasOwnProperty.call(res, m)) {
                var deepKey = "".concat(newKeyToUse).concat(keySeparator).concat(m);
                copy[m] = this.translate(deepKey, _objectSpread({}, options, {
                  joinArrays: false,
                  ns: namespaces
                }));
                if (copy[m] === deepKey) copy[m] = res[m];
              }
            }

            res = copy;
          }
        } else if (handleAsObjectInI18nFormat && typeof joinArrays === 'string' && resType === '[object Array]') {
          res = res.join(joinArrays);
          if (res) res = this.extendTranslation(res, keys, options, lastKey);
        } else {
          var usedDefault = false;
          var usedKey = false;
          var needsPluralHandling = options.count !== undefined && typeof options.count !== 'string';
          var hasDefaultValue = Translator.hasDefaultValue(options);
          var defaultValueSuffix = needsPluralHandling ? this.pluralResolver.getSuffix(lng, options.count) : '';
          var defaultValue = options["defaultValue".concat(defaultValueSuffix)] || options.defaultValue;

          if (!this.isValidLookup(res) && hasDefaultValue) {
            usedDefault = true;
            res = defaultValue;
          }

          if (!this.isValidLookup(res)) {
            usedKey = true;
            res = key;
          }

          var updateMissing = hasDefaultValue && defaultValue !== res && this.options.updateMissing;

          if (usedKey || usedDefault || updateMissing) {
            this.logger.log(updateMissing ? 'updateKey' : 'missingKey', lng, namespace, key, updateMissing ? defaultValue : res);

            if (keySeparator) {
              var fk = this.resolve(key, _objectSpread({}, options, {
                keySeparator: false
              }));
              if (fk && fk.res) this.logger.warn('Seems the loaded translations were in flat JSON format instead of nested. Either set keySeparator: false on init or make sure your translations are published in nested format.');
            }

            var lngs = [];
            var fallbackLngs = this.languageUtils.getFallbackCodes(this.options.fallbackLng, options.lng || this.language);

            if (this.options.saveMissingTo === 'fallback' && fallbackLngs && fallbackLngs[0]) {
              for (var i = 0; i < fallbackLngs.length; i++) {
                lngs.push(fallbackLngs[i]);
              }
            } else if (this.options.saveMissingTo === 'all') {
              lngs = this.languageUtils.toResolveHierarchy(options.lng || this.language);
            } else {
              lngs.push(options.lng || this.language);
            }

            var send = function send(l, k, fallbackValue) {
              if (_this2.options.missingKeyHandler) {
                _this2.options.missingKeyHandler(l, namespace, k, updateMissing ? fallbackValue : res, updateMissing, options);
              } else if (_this2.backendConnector && _this2.backendConnector.saveMissing) {
                _this2.backendConnector.saveMissing(l, namespace, k, updateMissing ? fallbackValue : res, updateMissing, options);
              }

              _this2.emit('missingKey', l, namespace, k, res);
            };

            if (this.options.saveMissing) {
              if (this.options.saveMissingPlurals && needsPluralHandling) {
                lngs.forEach(function (language) {
                  _this2.pluralResolver.getSuffixes(language).forEach(function (suffix) {
                    send([language], key + suffix, options["defaultValue".concat(suffix)] || defaultValue);
                  });
                });
              } else {
                send(lngs, key, defaultValue);
              }
            }
          }

          res = this.extendTranslation(res, keys, options, resolved, lastKey);
          if (usedKey && res === key && this.options.appendNamespaceToMissingKey) res = "".concat(namespace, ":").concat(key);
          if (usedKey && this.options.parseMissingKeyHandler) res = this.options.parseMissingKeyHandler(res);
        }

        return res;
      }
    }, {
      key: "extendTranslation",
      value: function extendTranslation(res, key, options, resolved, lastKey) {
        var _this3 = this;

        if (this.i18nFormat && this.i18nFormat.parse) {
          res = this.i18nFormat.parse(res, options, resolved.usedLng, resolved.usedNS, resolved.usedKey, {
            resolved: resolved
          });
        } else if (!options.skipInterpolation) {
          if (options.interpolation) this.interpolator.init(_objectSpread({}, options, {
            interpolation: _objectSpread({}, this.options.interpolation, options.interpolation)
          }));
          var skipOnVariables = options.interpolation && options.interpolation.skipOnVariables || this.options.interpolation.skipOnVariables;
          var nestBef;

          if (skipOnVariables) {
            var nb = res.match(this.interpolator.nestingRegexp);
            nestBef = nb && nb.length;
          }

          var data = options.replace && typeof options.replace !== 'string' ? options.replace : options;
          if (this.options.interpolation.defaultVariables) data = _objectSpread({}, this.options.interpolation.defaultVariables, data);
          res = this.interpolator.interpolate(res, data, options.lng || this.language, options);

          if (skipOnVariables) {
            var na = res.match(this.interpolator.nestingRegexp);
            var nestAft = na && na.length;
            if (nestBef < nestAft) options.nest = false;
          }

          if (options.nest !== false) res = this.interpolator.nest(res, function () {
            for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
              args[_key] = arguments[_key];
            }

            if (lastKey && lastKey[0] === args[0] && !options.context) {
              _this3.logger.warn("It seems you are nesting recursively key: ".concat(args[0], " in key: ").concat(key[0]));

              return null;
            }

            return _this3.translate.apply(_this3, args.concat([key]));
          }, options);
          if (options.interpolation) this.interpolator.reset();
        }

        var postProcess = options.postProcess || this.options.postProcess;
        var postProcessorNames = typeof postProcess === 'string' ? [postProcess] : postProcess;

        if (res !== undefined && res !== null && postProcessorNames && postProcessorNames.length && options.applyPostProcessor !== false) {
          res = postProcessor.handle(postProcessorNames, res, key, this.options && this.options.postProcessPassResolved ? _objectSpread({
            i18nResolved: resolved
          }, options) : options, this);
        }

        return res;
      }
    }, {
      key: "resolve",
      value: function resolve(keys) {
        var _this4 = this;

        var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
        var found;
        var usedKey;
        var exactUsedKey;
        var usedLng;
        var usedNS;
        if (typeof keys === 'string') keys = [keys];
        keys.forEach(function (k) {
          if (_this4.isValidLookup(found)) return;

          var extracted = _this4.extractFromKey(k, options);

          var key = extracted.key;
          usedKey = key;
          var namespaces = extracted.namespaces;
          if (_this4.options.fallbackNS) namespaces = namespaces.concat(_this4.options.fallbackNS);
          var needsPluralHandling = options.count !== undefined && typeof options.count !== 'string';
          var needsContextHandling = options.context !== undefined && typeof options.context === 'string' && options.context !== '';
          var codes = options.lngs ? options.lngs : _this4.languageUtils.toResolveHierarchy(options.lng || _this4.language, options.fallbackLng);
          namespaces.forEach(function (ns) {
            if (_this4.isValidLookup(found)) return;
            usedNS = ns;

            if (!checkedLoadedFor["".concat(codes[0], "-").concat(ns)] && _this4.utils && _this4.utils.hasLoadedNamespace && !_this4.utils.hasLoadedNamespace(usedNS)) {
              checkedLoadedFor["".concat(codes[0], "-").concat(ns)] = true;

              _this4.logger.warn("key \"".concat(usedKey, "\" for languages \"").concat(codes.join(', '), "\" won't get resolved as namespace \"").concat(usedNS, "\" was not yet loaded"), 'This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!');
            }

            codes.forEach(function (code) {
              if (_this4.isValidLookup(found)) return;
              usedLng = code;
              var finalKey = key;
              var finalKeys = [finalKey];

              if (_this4.i18nFormat && _this4.i18nFormat.addLookupKeys) {
                _this4.i18nFormat.addLookupKeys(finalKeys, key, code, ns, options);
              } else {
                var pluralSuffix;
                if (needsPluralHandling) pluralSuffix = _this4.pluralResolver.getSuffix(code, options.count);
                if (needsPluralHandling && needsContextHandling) finalKeys.push(finalKey + pluralSuffix);
                if (needsContextHandling) finalKeys.push(finalKey += "".concat(_this4.options.contextSeparator).concat(options.context));
                if (needsPluralHandling) finalKeys.push(finalKey += pluralSuffix);
              }

              var possibleKey;

              while (possibleKey = finalKeys.pop()) {
                if (!_this4.isValidLookup(found)) {
                  exactUsedKey = possibleKey;
                  found = _this4.getResource(code, ns, possibleKey, options);
                }
              }
            });
          });
        });
        return {
          res: found,
          usedKey: usedKey,
          exactUsedKey: exactUsedKey,
          usedLng: usedLng,
          usedNS: usedNS
        };
      }
    }, {
      key: "isValidLookup",
      value: function isValidLookup(res) {
        return res !== undefined && !(!this.options.returnNull && res === null) && !(!this.options.returnEmptyString && res === '');
      }
    }, {
      key: "getResource",
      value: function getResource(code, ns, key) {
        var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
        if (this.i18nFormat && this.i18nFormat.getResource) return this.i18nFormat.getResource(code, ns, key, options);
        return this.resourceStore.getResource(code, ns, key, options);
      }
    }], [{
      key: "hasDefaultValue",
      value: function hasDefaultValue(options) {
        var prefix = 'defaultValue';

        for (var option in options) {
          if (Object.prototype.hasOwnProperty.call(options, option) && prefix === option.substring(0, prefix.length) && undefined !== options[option]) {
            return true;
          }
        }

        return false;
      }
    }]);

    return Translator;
  }(EventEmitter);

  function capitalize(string) {
    return string.charAt(0).toUpperCase() + string.slice(1);
  }

  var LanguageUtil = function () {
    function LanguageUtil(options) {
      _classCallCheck(this, LanguageUtil);

      this.options = options;
      this.whitelist = this.options.supportedLngs || false;
      this.supportedLngs = this.options.supportedLngs || false;
      this.logger = baseLogger.create('languageUtils');
    }

    _createClass(LanguageUtil, [{
      key: "getScriptPartFromCode",
      value: function getScriptPartFromCode(code) {
        if (!code || code.indexOf('-') < 0) return null;
        var p = code.split('-');
        if (p.length === 2) return null;
        p.pop();
        if (p[p.length - 1].toLowerCase() === 'x') return null;
        return this.formatLanguageCode(p.join('-'));
      }
    }, {
      key: "getLanguagePartFromCode",
      value: function getLanguagePartFromCode(code) {
        if (!code || code.indexOf('-') < 0) return code;
        var p = code.split('-');
        return this.formatLanguageCode(p[0]);
      }
    }, {
      key: "formatLanguageCode",
      value: function formatLanguageCode(code) {
        if (typeof code === 'string' && code.indexOf('-') > -1) {
          var specialCases = ['hans', 'hant', 'latn', 'cyrl', 'cans', 'mong', 'arab'];
          var p = code.split('-');

          if (this.options.lowerCaseLng) {
            p = p.map(function (part) {
              return part.toLowerCase();
            });
          } else if (p.length === 2) {
            p[0] = p[0].toLowerCase();
            p[1] = p[1].toUpperCase();
            if (specialCases.indexOf(p[1].toLowerCase()) > -1) p[1] = capitalize(p[1].toLowerCase());
          } else if (p.length === 3) {
            p[0] = p[0].toLowerCase();
            if (p[1].length === 2) p[1] = p[1].toUpperCase();
            if (p[0] !== 'sgn' && p[2].length === 2) p[2] = p[2].toUpperCase();
            if (specialCases.indexOf(p[1].toLowerCase()) > -1) p[1] = capitalize(p[1].toLowerCase());
            if (specialCases.indexOf(p[2].toLowerCase()) > -1) p[2] = capitalize(p[2].toLowerCase());
          }

          return p.join('-');
        }

        return this.options.cleanCode || this.options.lowerCaseLng ? code.toLowerCase() : code;
      }
    }, {
      key: "isWhitelisted",
      value: function isWhitelisted(code) {
        this.logger.deprecate('languageUtils.isWhitelisted', 'function "isWhitelisted" will be renamed to "isSupportedCode" in the next major - please make sure to rename it\'s usage asap.');
        return this.isSupportedCode(code);
      }
    }, {
      key: "isSupportedCode",
      value: function isSupportedCode(code) {
        if (this.options.load === 'languageOnly' || this.options.nonExplicitSupportedLngs) {
          code = this.getLanguagePartFromCode(code);
        }

        return !this.supportedLngs || !this.supportedLngs.length || this.supportedLngs.indexOf(code) > -1;
      }
    }, {
      key: "getBestMatchFromCodes",
      value: function getBestMatchFromCodes(codes) {
        var _this = this;

        if (!codes) return null;
        var found;
        codes.forEach(function (code) {
          if (found) return;

          var cleanedLng = _this.formatLanguageCode(code);

          if (!_this.options.supportedLngs || _this.isSupportedCode(cleanedLng)) found = cleanedLng;
        });

        if (!found && this.options.supportedLngs) {
          codes.forEach(function (code) {
            if (found) return;

            var lngOnly = _this.getLanguagePartFromCode(code);

            if (_this.isSupportedCode(lngOnly)) return found = lngOnly;
            found = _this.options.supportedLngs.find(function (supportedLng) {
              if (supportedLng.indexOf(lngOnly) === 0) return supportedLng;
            });
          });
        }

        if (!found) found = this.getFallbackCodes(this.options.fallbackLng)[0];
        return found;
      }
    }, {
      key: "getFallbackCodes",
      value: function getFallbackCodes(fallbacks, code) {
        if (!fallbacks) return [];
        if (typeof fallbacks === 'function') fallbacks = fallbacks(code);
        if (typeof fallbacks === 'string') fallbacks = [fallbacks];
        if (Object.prototype.toString.apply(fallbacks) === '[object Array]') return fallbacks;
        if (!code) return fallbacks["default"] || [];
        var found = fallbacks[code];
        if (!found) found = fallbacks[this.getScriptPartFromCode(code)];
        if (!found) found = fallbacks[this.formatLanguageCode(code)];
        if (!found) found = fallbacks[this.getLanguagePartFromCode(code)];
        if (!found) found = fallbacks["default"];
        return found || [];
      }
    }, {
      key: "toResolveHierarchy",
      value: function toResolveHierarchy(code, fallbackCode) {
        var _this2 = this;

        var fallbackCodes = this.getFallbackCodes(fallbackCode || this.options.fallbackLng || [], code);
        var codes = [];

        var addCode = function addCode(c) {
          if (!c) return;

          if (_this2.isSupportedCode(c)) {
            codes.push(c);
          } else {
            _this2.logger.warn("rejecting language code not found in supportedLngs: ".concat(c));
          }
        };

        if (typeof code === 'string' && code.indexOf('-') > -1) {
          if (this.options.load !== 'languageOnly') addCode(this.formatLanguageCode(code));
          if (this.options.load !== 'languageOnly' && this.options.load !== 'currentOnly') addCode(this.getScriptPartFromCode(code));
          if (this.options.load !== 'currentOnly') addCode(this.getLanguagePartFromCode(code));
        } else if (typeof code === 'string') {
          addCode(this.formatLanguageCode(code));
        }

        fallbackCodes.forEach(function (fc) {
          if (codes.indexOf(fc) < 0) addCode(_this2.formatLanguageCode(fc));
        });
        return codes;
      }
    }]);

    return LanguageUtil;
  }();

  var sets = [{
    lngs: ['ach', 'ak', 'am', 'arn', 'br', 'fil', 'gun', 'ln', 'mfe', 'mg', 'mi', 'oc', 'pt', 'pt-BR', 'tg', 'tl', 'ti', 'tr', 'uz', 'wa'],
    nr: [1, 2],
    fc: 1
  }, {
    lngs: ['af', 'an', 'ast', 'az', 'bg', 'bn', 'ca', 'da', 'de', 'dev', 'el', 'en', 'eo', 'es', 'et', 'eu', 'fi', 'fo', 'fur', 'fy', 'gl', 'gu', 'ha', 'hi', 'hu', 'hy', 'ia', 'it', 'kn', 'ku', 'lb', 'mai', 'ml', 'mn', 'mr', 'nah', 'nap', 'nb', 'ne', 'nl', 'nn', 'no', 'nso', 'pa', 'pap', 'pms', 'ps', 'pt-PT', 'rm', 'sco', 'se', 'si', 'so', 'son', 'sq', 'sv', 'sw', 'ta', 'te', 'tk', 'ur', 'yo'],
    nr: [1, 2],
    fc: 2
  }, {
    lngs: ['ay', 'bo', 'cgg', 'fa', 'ht', 'id', 'ja', 'jbo', 'ka', 'kk', 'km', 'ko', 'ky', 'lo', 'ms', 'sah', 'su', 'th', 'tt', 'ug', 'vi', 'wo', 'zh'],
    nr: [1],
    fc: 3
  }, {
    lngs: ['be', 'bs', 'cnr', 'dz', 'hr', 'ru', 'sr', 'uk'],
    nr: [1, 2, 5],
    fc: 4
  }, {
    lngs: ['ar'],
    nr: [0, 1, 2, 3, 11, 100],
    fc: 5
  }, {
    lngs: ['cs', 'sk'],
    nr: [1, 2, 5],
    fc: 6
  }, {
    lngs: ['csb', 'pl'],
    nr: [1, 2, 5],
    fc: 7
  }, {
    lngs: ['cy'],
    nr: [1, 2, 3, 8],
    fc: 8
  }, {
    lngs: ['fr'],
    nr: [1, 2],
    fc: 9
  }, {
    lngs: ['ga'],
    nr: [1, 2, 3, 7, 11],
    fc: 10
  }, {
    lngs: ['gd'],
    nr: [1, 2, 3, 20],
    fc: 11
  }, {
    lngs: ['is'],
    nr: [1, 2],
    fc: 12
  }, {
    lngs: ['jv'],
    nr: [0, 1],
    fc: 13
  }, {
    lngs: ['kw'],
    nr: [1, 2, 3, 4],
    fc: 14
  }, {
    lngs: ['lt'],
    nr: [1, 2, 10],
    fc: 15
  }, {
    lngs: ['lv'],
    nr: [1, 2, 0],
    fc: 16
  }, {
    lngs: ['mk'],
    nr: [1, 2],
    fc: 17
  }, {
    lngs: ['mnk'],
    nr: [0, 1, 2],
    fc: 18
  }, {
    lngs: ['mt'],
    nr: [1, 2, 11, 20],
    fc: 19
  }, {
    lngs: ['or'],
    nr: [2, 1],
    fc: 2
  }, {
    lngs: ['ro'],
    nr: [1, 2, 20],
    fc: 20
  }, {
    lngs: ['sl'],
    nr: [5, 1, 2, 3],
    fc: 21
  }, {
    lngs: ['he', 'iw'],
    nr: [1, 2, 20, 21],
    fc: 22
  }];
  var _rulesPluralsTypes = {
    1: function _(n) {
      return Number(n > 1);
    },
    2: function _(n) {
      return Number(n != 1);
    },
    3: function _(n) {
      return 0;
    },
    4: function _(n) {
      return Number(n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2);
    },
    5: function _(n) {
      return Number(n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5);
    },
    6: function _(n) {
      return Number(n == 1 ? 0 : n >= 2 && n <= 4 ? 1 : 2);
    },
    7: function _(n) {
      return Number(n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2);
    },
    8: function _(n) {
      return Number(n == 1 ? 0 : n == 2 ? 1 : n != 8 && n != 11 ? 2 : 3);
    },
    9: function _(n) {
      return Number(n >= 2);
    },
    10: function _(n) {
      return Number(n == 1 ? 0 : n == 2 ? 1 : n < 7 ? 2 : n < 11 ? 3 : 4);
    },
    11: function _(n) {
      return Number(n == 1 || n == 11 ? 0 : n == 2 || n == 12 ? 1 : n > 2 && n < 20 ? 2 : 3);
    },
    12: function _(n) {
      return Number(n % 10 != 1 || n % 100 == 11);
    },
    13: function _(n) {
      return Number(n !== 0);
    },
    14: function _(n) {
      return Number(n == 1 ? 0 : n == 2 ? 1 : n == 3 ? 2 : 3);
    },
    15: function _(n) {
      return Number(n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2);
    },
    16: function _(n) {
      return Number(n % 10 == 1 && n % 100 != 11 ? 0 : n !== 0 ? 1 : 2);
    },
    17: function _(n) {
      return Number(n == 1 || n % 10 == 1 && n % 100 != 11 ? 0 : 1);
    },
    18: function _(n) {
      return Number(n == 0 ? 0 : n == 1 ? 1 : 2);
    },
    19: function _(n) {
      return Number(n == 1 ? 0 : n == 0 || n % 100 > 1 && n % 100 < 11 ? 1 : n % 100 > 10 && n % 100 < 20 ? 2 : 3);
    },
    20: function _(n) {
      return Number(n == 1 ? 0 : n == 0 || n % 100 > 0 && n % 100 < 20 ? 1 : 2);
    },
    21: function _(n) {
      return Number(n % 100 == 1 ? 1 : n % 100 == 2 ? 2 : n % 100 == 3 || n % 100 == 4 ? 3 : 0);
    },
    22: function _(n) {
      return Number(n == 1 ? 0 : n == 2 ? 1 : (n < 0 || n > 10) && n % 10 == 0 ? 2 : 3);
    }
  };

  function createRules() {
    var rules = {};
    sets.forEach(function (set) {
      set.lngs.forEach(function (l) {
        rules[l] = {
          numbers: set.nr,
          plurals: _rulesPluralsTypes[set.fc]
        };
      });
    });
    return rules;
  }

  var PluralResolver = function () {
    function PluralResolver(languageUtils) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

      _classCallCheck(this, PluralResolver);

      this.languageUtils = languageUtils;
      this.options = options;
      this.logger = baseLogger.create('pluralResolver');
      this.rules = createRules();
    }

    _createClass(PluralResolver, [{
      key: "addRule",
      value: function addRule(lng, obj) {
        this.rules[lng] = obj;
      }
    }, {
      key: "getRule",
      value: function getRule(code) {
        return this.rules[code] || this.rules[this.languageUtils.getLanguagePartFromCode(code)];
      }
    }, {
      key: "needsPlural",
      value: function needsPlural(code) {
        var rule = this.getRule(code);
        return rule && rule.numbers.length > 1;
      }
    }, {
      key: "getPluralFormsOfKey",
      value: function getPluralFormsOfKey(code, key) {
        return this.getSuffixes(code).map(function (suffix) {
          return key + suffix;
        });
      }
    }, {
      key: "getSuffixes",
      value: function getSuffixes(code) {
        var _this = this;

        var rule = this.getRule(code);

        if (!rule) {
          return [];
        }

        return rule.numbers.map(function (number) {
          return _this.getSuffix(code, number);
        });
      }
    }, {
      key: "getSuffix",
      value: function getSuffix(code, count) {
        var _this2 = this;

        var rule = this.getRule(code);

        if (rule) {
          var idx = rule.noAbs ? rule.plurals(count) : rule.plurals(Math.abs(count));
          var suffix = rule.numbers[idx];

          if (this.options.simplifyPluralSuffix && rule.numbers.length === 2 && rule.numbers[0] === 1) {
            if (suffix === 2) {
              suffix = 'plural';
            } else if (suffix === 1) {
              suffix = '';
            }
          }

          var returnSuffix = function returnSuffix() {
            return _this2.options.prepend && suffix.toString() ? _this2.options.prepend + suffix.toString() : suffix.toString();
          };

          if (this.options.compatibilityJSON === 'v1') {
            if (suffix === 1) return '';
            if (typeof suffix === 'number') return "_plural_".concat(suffix.toString());
            return returnSuffix();
          } else if (this.options.compatibilityJSON === 'v2') {
            return returnSuffix();
          } else if (this.options.simplifyPluralSuffix && rule.numbers.length === 2 && rule.numbers[0] === 1) {
            return returnSuffix();
          }

          return this.options.prepend && idx.toString() ? this.options.prepend + idx.toString() : idx.toString();
        }

        this.logger.warn("no plural rule found for: ".concat(code));
        return '';
      }
    }]);

    return PluralResolver;
  }();

  var Interpolator = function () {
    function Interpolator() {
      var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};

      _classCallCheck(this, Interpolator);

      this.logger = baseLogger.create('interpolator');
      this.options = options;

      this.format = options.interpolation && options.interpolation.format || function (value) {
        return value;
      };

      this.init(options);
    }

    _createClass(Interpolator, [{
      key: "init",
      value: function init() {
        var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
        if (!options.interpolation) options.interpolation = {
          escapeValue: true
        };
        var iOpts = options.interpolation;
        this.escape = iOpts.escape !== undefined ? iOpts.escape : escape;
        this.escapeValue = iOpts.escapeValue !== undefined ? iOpts.escapeValue : true;
        this.useRawValueToEscape = iOpts.useRawValueToEscape !== undefined ? iOpts.useRawValueToEscape : false;
        this.prefix = iOpts.prefix ? regexEscape(iOpts.prefix) : iOpts.prefixEscaped || '{{';
        this.suffix = iOpts.suffix ? regexEscape(iOpts.suffix) : iOpts.suffixEscaped || '}}';
        this.formatSeparator = iOpts.formatSeparator ? iOpts.formatSeparator : iOpts.formatSeparator || ',';
        this.unescapePrefix = iOpts.unescapeSuffix ? '' : iOpts.unescapePrefix || '-';
        this.unescapeSuffix = this.unescapePrefix ? '' : iOpts.unescapeSuffix || '';
        this.nestingPrefix = iOpts.nestingPrefix ? regexEscape(iOpts.nestingPrefix) : iOpts.nestingPrefixEscaped || regexEscape('$t(');
        this.nestingSuffix = iOpts.nestingSuffix ? regexEscape(iOpts.nestingSuffix) : iOpts.nestingSuffixEscaped || regexEscape(')');
        this.nestingOptionsSeparator = iOpts.nestingOptionsSeparator ? iOpts.nestingOptionsSeparator : iOpts.nestingOptionsSeparator || ',';
        this.maxReplaces = iOpts.maxReplaces ? iOpts.maxReplaces : 1000;
        this.alwaysFormat = iOpts.alwaysFormat !== undefined ? iOpts.alwaysFormat : false;
        this.resetRegExp();
      }
    }, {
      key: "reset",
      value: function reset() {
        if (this.options) this.init(this.options);
      }
    }, {
      key: "resetRegExp",
      value: function resetRegExp() {
        var regexpStr = "".concat(this.prefix, "(.+?)").concat(this.suffix);
        this.regexp = new RegExp(regexpStr, 'g');
        var regexpUnescapeStr = "".concat(this.prefix).concat(this.unescapePrefix, "(.+?)").concat(this.unescapeSuffix).concat(this.suffix);
        this.regexpUnescape = new RegExp(regexpUnescapeStr, 'g');
        var nestingRegexpStr = "".concat(this.nestingPrefix, "(.+?)").concat(this.nestingSuffix);
        this.nestingRegexp = new RegExp(nestingRegexpStr, 'g');
      }
    }, {
      key: "interpolate",
      value: function interpolate(str, data, lng, options) {
        var _this = this;

        var match;
        var value;
        var replaces;
        var defaultData = this.options && this.options.interpolation && this.options.interpolation.defaultVariables || {};

        function regexSafe(val) {
          return val.replace(/\$/g, '$$$$');
        }

        var handleFormat = function handleFormat(key) {
          if (key.indexOf(_this.formatSeparator) < 0) {
            var path = getPathWithDefaults(data, defaultData, key);
            return _this.alwaysFormat ? _this.format(path, undefined, lng) : path;
          }

          var p = key.split(_this.formatSeparator);
          var k = p.shift().trim();
          var f = p.join(_this.formatSeparator).trim();
          return _this.format(getPathWithDefaults(data, defaultData, k), f, lng, options);
        };

        this.resetRegExp();
        var missingInterpolationHandler = options && options.missingInterpolationHandler || this.options.missingInterpolationHandler;
        var skipOnVariables = options && options.interpolation && options.interpolation.skipOnVariables || this.options.interpolation.skipOnVariables;
        var todos = [{
          regex: this.regexpUnescape,
          safeValue: function safeValue(val) {
            return regexSafe(val);
          }
        }, {
          regex: this.regexp,
          safeValue: function safeValue(val) {
            return _this.escapeValue ? regexSafe(_this.escape(val)) : regexSafe(val);
          }
        }];
        todos.forEach(function (todo) {
          replaces = 0;

          while (match = todo.regex.exec(str)) {
            value = handleFormat(match[1].trim());

            if (value === undefined) {
              if (typeof missingInterpolationHandler === 'function') {
                var temp = missingInterpolationHandler(str, match, options);
                value = typeof temp === 'string' ? temp : '';
              } else if (skipOnVariables) {
                value = match[0];
                continue;
              } else {
                _this.logger.warn("missed to pass in variable ".concat(match[1], " for interpolating ").concat(str));

                value = '';
              }
            } else if (typeof value !== 'string' && !_this.useRawValueToEscape) {
              value = makeString(value);
            }

            str = str.replace(match[0], todo.safeValue(value));
            todo.regex.lastIndex = 0;
            replaces++;

            if (replaces >= _this.maxReplaces) {
              break;
            }
          }
        });
        return str;
      }
    }, {
      key: "nest",
      value: function nest(str, fc) {
        var _this2 = this;

        var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
        var match;
        var value;

        var clonedOptions = _objectSpread({}, options);

        clonedOptions.applyPostProcessor = false;
        delete clonedOptions.defaultValue;

        function handleHasOptions(key, inheritedOptions) {
          var sep = this.nestingOptionsSeparator;
          if (key.indexOf(sep) < 0) return key;
          var c = key.split(new RegExp("".concat(sep, "[ ]*{")));
          var optionsString = "{".concat(c[1]);
          key = c[0];
          optionsString = this.interpolate(optionsString, clonedOptions);
          optionsString = optionsString.replace(/'/g, '"');

          try {
            clonedOptions = JSON.parse(optionsString);
            if (inheritedOptions) clonedOptions = _objectSpread({}, inheritedOptions, clonedOptions);
          } catch (e) {
            this.logger.warn("failed parsing options string in nesting for key ".concat(key), e);
            return "".concat(key).concat(sep).concat(optionsString);
          }

          delete clonedOptions.defaultValue;
          return key;
        }

        while (match = this.nestingRegexp.exec(str)) {
          var formatters = [];
          var doReduce = false;

          if (match[0].includes(this.formatSeparator) && !/{.*}/.test(match[1])) {
            var r = match[1].split(this.formatSeparator).map(function (elem) {
              return elem.trim();
            });
            match[1] = r.shift();
            formatters = r;
            doReduce = true;
          }

          value = fc(handleHasOptions.call(this, match[1].trim(), clonedOptions), clonedOptions);
          if (value && match[0] === str && typeof value !== 'string') return value;
          if (typeof value !== 'string') value = makeString(value);

          if (!value) {
            this.logger.warn("missed to resolve ".concat(match[1], " for nesting ").concat(str));
            value = '';
          }

          if (doReduce) {
            value = formatters.reduce(function (v, f) {
              return _this2.format(v, f, options.lng, options);
            }, value.trim());
          }

          str = str.replace(match[0], value);
          this.regexp.lastIndex = 0;
        }

        return str;
      }
    }]);

    return Interpolator;
  }();

  function remove(arr, what) {
    var found = arr.indexOf(what);

    while (found !== -1) {
      arr.splice(found, 1);
      found = arr.indexOf(what);
    }
  }

  var Connector = function (_EventEmitter) {
    _inherits(Connector, _EventEmitter);

    function Connector(backend, store, services) {
      var _this;

      var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};

      _classCallCheck(this, Connector);

      _this = _possibleConstructorReturn(this, _getPrototypeOf(Connector).call(this));

      if (isIE10) {
        EventEmitter.call(_assertThisInitialized(_this));
      }

      _this.backend = backend;
      _this.store = store;
      _this.services = services;
      _this.languageUtils = services.languageUtils;
      _this.options = options;
      _this.logger = baseLogger.create('backendConnector');
      _this.state = {};
      _this.queue = [];

      if (_this.backend && _this.backend.init) {
        _this.backend.init(services, options.backend, options);
      }

      return _this;
    }

    _createClass(Connector, [{
      key: "queueLoad",
      value: function queueLoad(languages, namespaces, options, callback) {
        var _this2 = this;

        var toLoad = [];
        var pending = [];
        var toLoadLanguages = [];
        var toLoadNamespaces = [];
        languages.forEach(function (lng) {
          var hasAllNamespaces = true;
          namespaces.forEach(function (ns) {
            var name = "".concat(lng, "|").concat(ns);

            if (!options.reload && _this2.store.hasResourceBundle(lng, ns)) {
              _this2.state[name] = 2;
            } else if (_this2.state[name] < 0) ; else if (_this2.state[name] === 1) {
              if (pending.indexOf(name) < 0) pending.push(name);
            } else {
              _this2.state[name] = 1;
              hasAllNamespaces = false;
              if (pending.indexOf(name) < 0) pending.push(name);
              if (toLoad.indexOf(name) < 0) toLoad.push(name);
              if (toLoadNamespaces.indexOf(ns) < 0) toLoadNamespaces.push(ns);
            }
          });
          if (!hasAllNamespaces) toLoadLanguages.push(lng);
        });

        if (toLoad.length || pending.length) {
          this.queue.push({
            pending: pending,
            loaded: {},
            errors: [],
            callback: callback
          });
        }

        return {
          toLoad: toLoad,
          pending: pending,
          toLoadLanguages: toLoadLanguages,
          toLoadNamespaces: toLoadNamespaces
        };
      }
    }, {
      key: "loaded",
      value: function loaded(name, err, data) {
        var s = name.split('|');
        var lng = s[0];
        var ns = s[1];
        if (err) this.emit('failedLoading', lng, ns, err);

        if (data) {
          this.store.addResourceBundle(lng, ns, data);
        }

        this.state[name] = err ? -1 : 2;
        var loaded = {};
        this.queue.forEach(function (q) {
          pushPath(q.loaded, [lng], ns);
          remove(q.pending, name);
          if (err) q.errors.push(err);

          if (q.pending.length === 0 && !q.done) {
            Object.keys(q.loaded).forEach(function (l) {
              if (!loaded[l]) loaded[l] = [];

              if (q.loaded[l].length) {
                q.loaded[l].forEach(function (ns) {
                  if (loaded[l].indexOf(ns) < 0) loaded[l].push(ns);
                });
              }
            });
            q.done = true;

            if (q.errors.length) {
              q.callback(q.errors);
            } else {
              q.callback();
            }
          }
        });
        this.emit('loaded', loaded);
        this.queue = this.queue.filter(function (q) {
          return !q.done;
        });
      }
    }, {
      key: "read",
      value: function read(lng, ns, fcName) {
        var _this3 = this;

        var tried = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0;
        var wait = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 350;
        var callback = arguments.length > 5 ? arguments[5] : undefined;
        if (!lng.length) return callback(null, {});
        return this.backend[fcName](lng, ns, function (err, data) {
          if (err && data && tried < 5) {
            setTimeout(function () {
              _this3.read.call(_this3, lng, ns, fcName, tried + 1, wait * 2, callback);
            }, wait);
            return;
          }

          callback(err, data);
        });
      }
    }, {
      key: "prepareLoading",
      value: function prepareLoading(languages, namespaces) {
        var _this4 = this;

        var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
        var callback = arguments.length > 3 ? arguments[3] : undefined;

        if (!this.backend) {
          this.logger.warn('No backend was added via i18next.use. Will not load resources.');
          return callback && callback();
        }

        if (typeof languages === 'string') languages = this.languageUtils.toResolveHierarchy(languages);
        if (typeof namespaces === 'string') namespaces = [namespaces];
        var toLoad = this.queueLoad(languages, namespaces, options, callback);

        if (!toLoad.toLoad.length) {
          if (!toLoad.pending.length) callback();
          return null;
        }

        toLoad.toLoad.forEach(function (name) {
          _this4.loadOne(name);
        });
      }
    }, {
      key: "load",
      value: function load(languages, namespaces, callback) {
        this.prepareLoading(languages, namespaces, {}, callback);
      }
    }, {
      key: "reload",
      value: function reload(languages, namespaces, callback) {
        this.prepareLoading(languages, namespaces, {
          reload: true
        }, callback);
      }
    }, {
      key: "loadOne",
      value: function loadOne(name) {
        var _this5 = this;

        var prefix = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
        var s = name.split('|');
        var lng = s[0];
        var ns = s[1];
        this.read(lng, ns, 'read', undefined, undefined, function (err, data) {
          if (err) _this5.logger.warn("".concat(prefix, "loading namespace ").concat(ns, " for language ").concat(lng, " failed"), err);
          if (!err && data) _this5.logger.log("".concat(prefix, "loaded namespace ").concat(ns, " for language ").concat(lng), data);

          _this5.loaded(name, err, data);
        });
      }
    }, {
      key: "saveMissing",
      value: function saveMissing(languages, namespace, key, fallbackValue, isUpdate) {
        var options = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : {};

        if (this.services.utils && this.services.utils.hasLoadedNamespace && !this.services.utils.hasLoadedNamespace(namespace)) {
          this.logger.warn("did not save key \"".concat(key, "\" as the namespace \"").concat(namespace, "\" was not yet loaded"), 'This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!');
          return;
        }

        if (key === undefined || key === null || key === '') return;

        if (this.backend && this.backend.create) {
          this.backend.create(languages, namespace, key, fallbackValue, null, _objectSpread({}, options, {
            isUpdate: isUpdate
          }));
        }

        if (!languages || !languages[0]) return;
        this.store.addResource(languages[0], namespace, key, fallbackValue);
      }
    }]);

    return Connector;
  }(EventEmitter);

  function get() {
    return {
      debug: false,
      initImmediate: true,
      ns: ['translation'],
      defaultNS: ['translation'],
      fallbackLng: ['dev'],
      fallbackNS: false,
      whitelist: false,
      nonExplicitWhitelist: false,
      supportedLngs: false,
      nonExplicitSupportedLngs: false,
      load: 'all',
      preload: false,
      simplifyPluralSuffix: true,
      keySeparator: '.',
      nsSeparator: ':',
      pluralSeparator: '_',
      contextSeparator: '_',
      partialBundledLanguages: false,
      saveMissing: false,
      updateMissing: false,
      saveMissingTo: 'fallback',
      saveMissingPlurals: true,
      missingKeyHandler: false,
      missingInterpolationHandler: false,
      postProcess: false,
      postProcessPassResolved: false,
      returnNull: true,
      returnEmptyString: true,
      returnObjects: false,
      joinArrays: false,
      returnedObjectHandler: false,
      parseMissingKeyHandler: false,
      appendNamespaceToMissingKey: false,
      appendNamespaceToCIMode: false,
      overloadTranslationOptionHandler: function handle(args) {
        var ret = {};
        if (_typeof(args[1]) === 'object') ret = args[1];
        if (typeof args[1] === 'string') ret.defaultValue = args[1];
        if (typeof args[2] === 'string') ret.tDescription = args[2];

        if (_typeof(args[2]) === 'object' || _typeof(args[3]) === 'object') {
          var options = args[3] || args[2];
          Object.keys(options).forEach(function (key) {
            ret[key] = options[key];
          });
        }

        return ret;
      },
      interpolation: {
        escapeValue: true,
        format: function format(value, _format, lng, options) {
          return value;
        },
        prefix: '{{',
        suffix: '}}',
        formatSeparator: ',',
        unescapePrefix: '-',
        nestingPrefix: '$t(',
        nestingSuffix: ')',
        nestingOptionsSeparator: ',',
        maxReplaces: 1000,
        skipOnVariables: false
      }
    };
  }
  function transformOptions(options) {
    if (typeof options.ns === 'string') options.ns = [options.ns];
    if (typeof options.fallbackLng === 'string') options.fallbackLng = [options.fallbackLng];
    if (typeof options.fallbackNS === 'string') options.fallbackNS = [options.fallbackNS];

    if (options.whitelist) {
      if (options.whitelist && options.whitelist.indexOf('cimode') < 0) {
        options.whitelist = options.whitelist.concat(['cimode']);
      }

      options.supportedLngs = options.whitelist;
    }

    if (options.nonExplicitWhitelist) {
      options.nonExplicitSupportedLngs = options.nonExplicitWhitelist;
    }

    if (options.supportedLngs && options.supportedLngs.indexOf('cimode') < 0) {
      options.supportedLngs = options.supportedLngs.concat(['cimode']);
    }

    return options;
  }

  function noop() {}

  var I18n = function (_EventEmitter) {
    _inherits(I18n, _EventEmitter);

    function I18n() {
      var _this;

      var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
      var callback = arguments.length > 1 ? arguments[1] : undefined;

      _classCallCheck(this, I18n);

      _this = _possibleConstructorReturn(this, _getPrototypeOf(I18n).call(this));

      if (isIE10) {
        EventEmitter.call(_assertThisInitialized(_this));
      }

      _this.options = transformOptions(options);
      _this.services = {};
      _this.logger = baseLogger;
      _this.modules = {
        external: []
      };

      if (callback && !_this.isInitialized && !options.isClone) {
        if (!_this.options.initImmediate) {
          _this.init(options, callback);

          return _possibleConstructorReturn(_this, _assertThisInitialized(_this));
        }

        setTimeout(function () {
          _this.init(options, callback);
        }, 0);
      }

      return _this;
    }

    _createClass(I18n, [{
      key: "init",
      value: function init() {
        var _this2 = this;

        var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
        var callback = arguments.length > 1 ? arguments[1] : undefined;

        if (typeof options === 'function') {
          callback = options;
          options = {};
        }

        if (options.whitelist && !options.supportedLngs) {
          this.logger.deprecate('whitelist', 'option "whitelist" will be renamed to "supportedLngs" in the next major - please make sure to rename this option asap.');
        }

        if (options.nonExplicitWhitelist && !options.nonExplicitSupportedLngs) {
          this.logger.deprecate('whitelist', 'options "nonExplicitWhitelist" will be renamed to "nonExplicitSupportedLngs" in the next major - please make sure to rename this option asap.');
        }

        this.options = _objectSpread({}, get(), this.options, transformOptions(options));
        this.format = this.options.interpolation.format;
        if (!callback) callback = noop;

        function createClassOnDemand(ClassOrObject) {
          if (!ClassOrObject) return null;
          if (typeof ClassOrObject === 'function') return new ClassOrObject();
          return ClassOrObject;
        }

        if (!this.options.isClone) {
          if (this.modules.logger) {
            baseLogger.init(createClassOnDemand(this.modules.logger), this.options);
          } else {
            baseLogger.init(null, this.options);
          }

          var lu = new LanguageUtil(this.options);
          this.store = new ResourceStore(this.options.resources, this.options);
          var s = this.services;
          s.logger = baseLogger;
          s.resourceStore = this.store;
          s.languageUtils = lu;
          s.pluralResolver = new PluralResolver(lu, {
            prepend: this.options.pluralSeparator,
            compatibilityJSON: this.options.compatibilityJSON,
            simplifyPluralSuffix: this.options.simplifyPluralSuffix
          });
          s.interpolator = new Interpolator(this.options);
          s.utils = {
            hasLoadedNamespace: this.hasLoadedNamespace.bind(this)
          };
          s.backendConnector = new Connector(createClassOnDemand(this.modules.backend), s.resourceStore, s, this.options);
          s.backendConnector.on('*', function (event) {
            for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
              args[_key - 1] = arguments[_key];
            }

            _this2.emit.apply(_this2, [event].concat(args));
          });

          if (this.modules.languageDetector) {
            s.languageDetector = createClassOnDemand(this.modules.languageDetector);
            s.languageDetector.init(s, this.options.detection, this.options);
          }

          if (this.modules.i18nFormat) {
            s.i18nFormat = createClassOnDemand(this.modules.i18nFormat);
            if (s.i18nFormat.init) s.i18nFormat.init(this);
          }

          this.translator = new Translator(this.services, this.options);
          this.translator.on('*', function (event) {
            for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
              args[_key2 - 1] = arguments[_key2];
            }

            _this2.emit.apply(_this2, [event].concat(args));
          });
          this.modules.external.forEach(function (m) {
            if (m.init) m.init(_this2);
          });
        }

        if (this.options.fallbackLng && !this.services.languageDetector && !this.options.lng) {
          var codes = this.services.languageUtils.getFallbackCodes(this.options.fallbackLng);
          if (codes.length > 0 && codes[0] !== 'dev') this.options.lng = codes[0];
        }

        if (!this.services.languageDetector && !this.options.lng) {
          this.logger.warn('init: no languageDetector is used and no lng is defined');
        }

        var storeApi = ['getResource', 'hasResourceBundle', 'getResourceBundle', 'getDataByLanguage'];
        storeApi.forEach(function (fcName) {
          _this2[fcName] = function () {
            var _this2$store;

            return (_this2$store = _this2.store)[fcName].apply(_this2$store, arguments);
          };
        });
        var storeApiChained = ['addResource', 'addResources', 'addResourceBundle', 'removeResourceBundle'];
        storeApiChained.forEach(function (fcName) {
          _this2[fcName] = function () {
            var _this2$store2;

            (_this2$store2 = _this2.store)[fcName].apply(_this2$store2, arguments);

            return _this2;
          };
        });
        var deferred = defer();

        var load = function load() {
          var finish = function finish(err, t) {
            if (_this2.isInitialized) _this2.logger.warn('init: i18next is already initialized. You should call init just once!');
            _this2.isInitialized = true;
            if (!_this2.options.isClone) _this2.logger.log('initialized', _this2.options);

            _this2.emit('initialized', _this2.options);

            deferred.resolve(t);
            callback(err, t);
          };

          if (_this2.languages && _this2.options.compatibilityAPI !== 'v1' && !_this2.isInitialized) return finish(null, _this2.t.bind(_this2));

          _this2.changeLanguage(_this2.options.lng, finish);
        };

        if (this.options.resources || !this.options.initImmediate) {
          load();
        } else {
          setTimeout(load, 0);
        }

        return deferred;
      }
    }, {
      key: "loadResources",
      value: function loadResources(language) {
        var _this3 = this;

        var callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop;
        var usedCallback = callback;
        var usedLng = typeof language === 'string' ? language : this.language;
        if (typeof language === 'function') usedCallback = language;

        if (!this.options.resources || this.options.partialBundledLanguages) {
          if (usedLng && usedLng.toLowerCase() === 'cimode') return usedCallback();
          var toLoad = [];

          var append = function append(lng) {
            if (!lng) return;

            var lngs = _this3.services.languageUtils.toResolveHierarchy(lng);

            lngs.forEach(function (l) {
              if (toLoad.indexOf(l) < 0) toLoad.push(l);
            });
          };

          if (!usedLng) {
            var fallbacks = this.services.languageUtils.getFallbackCodes(this.options.fallbackLng);
            fallbacks.forEach(function (l) {
              return append(l);
            });
          } else {
            append(usedLng);
          }

          if (this.options.preload) {
            this.options.preload.forEach(function (l) {
              return append(l);
            });
          }

          this.services.backendConnector.load(toLoad, this.options.ns, usedCallback);
        } else {
          usedCallback(null);
        }
      }
    }, {
      key: "reloadResources",
      value: function reloadResources(lngs, ns, callback) {
        var deferred = defer();
        if (!lngs) lngs = this.languages;
        if (!ns) ns = this.options.ns;
        if (!callback) callback = noop;
        this.services.backendConnector.reload(lngs, ns, function (err) {
          deferred.resolve();
          callback(err);
        });
        return deferred;
      }
    }, {
      key: "use",
      value: function use(module) {
        if (!module) throw new Error('You are passing an undefined module! Please check the object you are passing to i18next.use()');
        if (!module.type) throw new Error('You are passing a wrong module! Please check the object you are passing to i18next.use()');

        if (module.type === 'backend') {
          this.modules.backend = module;
        }

        if (module.type === 'logger' || module.log && module.warn && module.error) {
          this.modules.logger = module;
        }

        if (module.type === 'languageDetector') {
          this.modules.languageDetector = module;
        }

        if (module.type === 'i18nFormat') {
          this.modules.i18nFormat = module;
        }

        if (module.type === 'postProcessor') {
          postProcessor.addPostProcessor(module);
        }

        if (module.type === '3rdParty') {
          this.modules.external.push(module);
        }

        return this;
      }
    }, {
      key: "changeLanguage",
      value: function changeLanguage(lng, callback) {
        var _this4 = this;

        this.isLanguageChangingTo = lng;
        var deferred = defer();
        this.emit('languageChanging', lng);

        var done = function done(err, l) {
          if (l) {
            _this4.language = l;
            _this4.languages = _this4.services.languageUtils.toResolveHierarchy(l);

            _this4.translator.changeLanguage(l);

            _this4.isLanguageChangingTo = undefined;

            _this4.emit('languageChanged', l);

            _this4.logger.log('languageChanged', l);
          } else {
            _this4.isLanguageChangingTo = undefined;
          }

          deferred.resolve(function () {
            return _this4.t.apply(_this4, arguments);
          });
          if (callback) callback(err, function () {
            return _this4.t.apply(_this4, arguments);
          });
        };

        var setLng = function setLng(lngs) {
          var l = typeof lngs === 'string' ? lngs : _this4.services.languageUtils.getBestMatchFromCodes(lngs);

          if (l) {
            if (!_this4.language) {
              _this4.language = l;
              _this4.languages = _this4.services.languageUtils.toResolveHierarchy(l);
            }

            if (!_this4.translator.language) _this4.translator.changeLanguage(l);
            if (_this4.services.languageDetector) _this4.services.languageDetector.cacheUserLanguage(l);
          }

          _this4.loadResources(l, function (err) {
            done(err, l);
          });
        };

        if (!lng && this.services.languageDetector && !this.services.languageDetector.async) {
          setLng(this.services.languageDetector.detect());
        } else if (!lng && this.services.languageDetector && this.services.languageDetector.async) {
          this.services.languageDetector.detect(setLng);
        } else {
          setLng(lng);
        }

        return deferred;
      }
    }, {
      key: "getFixedT",
      value: function getFixedT(lng, ns) {
        var _this5 = this;

        var fixedT = function fixedT(key, opts) {
          var options;

          if (_typeof(opts) !== 'object') {
            for (var _len3 = arguments.length, rest = new Array(_len3 > 2 ? _len3 - 2 : 0), _key3 = 2; _key3 < _len3; _key3++) {
              rest[_key3 - 2] = arguments[_key3];
            }

            options = _this5.options.overloadTranslationOptionHandler([key, opts].concat(rest));
          } else {
            options = _objectSpread({}, opts);
          }

          options.lng = options.lng || fixedT.lng;
          options.lngs = options.lngs || fixedT.lngs;
          options.ns = options.ns || fixedT.ns;
          return _this5.t(key, options);
        };

        if (typeof lng === 'string') {
          fixedT.lng = lng;
        } else {
          fixedT.lngs = lng;
        }

        fixedT.ns = ns;
        return fixedT;
      }
    }, {
      key: "t",
      value: function t() {
        var _this$translator;

        return this.translator && (_this$translator = this.translator).translate.apply(_this$translator, arguments);
      }
    }, {
      key: "exists",
      value: function exists() {
        var _this$translator2;

        return this.translator && (_this$translator2 = this.translator).exists.apply(_this$translator2, arguments);
      }
    }, {
      key: "setDefaultNamespace",
      value: function setDefaultNamespace(ns) {
        this.options.defaultNS = ns;
      }
    }, {
      key: "hasLoadedNamespace",
      value: function hasLoadedNamespace(ns) {
        var _this6 = this;

        var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

        if (!this.isInitialized) {
          this.logger.warn('hasLoadedNamespace: i18next was not initialized', this.languages);
          return false;
        }

        if (!this.languages || !this.languages.length) {
          this.logger.warn('hasLoadedNamespace: i18n.languages were undefined or empty', this.languages);
          return false;
        }

        var lng = this.languages[0];
        var fallbackLng = this.options ? this.options.fallbackLng : false;
        var lastLng = this.languages[this.languages.length - 1];
        if (lng.toLowerCase() === 'cimode') return true;

        var loadNotPending = function loadNotPending(l, n) {
          var loadState = _this6.services.backendConnector.state["".concat(l, "|").concat(n)];

          return loadState === -1 || loadState === 2;
        };

        if (options.precheck) {
          var preResult = options.precheck(this, loadNotPending);
          if (preResult !== undefined) return preResult;
        }

        if (this.hasResourceBundle(lng, ns)) return true;
        if (!this.services.backendConnector.backend) return true;
        if (loadNotPending(lng, ns) && (!fallbackLng || loadNotPending(lastLng, ns))) return true;
        return false;
      }
    }, {
      key: "loadNamespaces",
      value: function loadNamespaces(ns, callback) {
        var _this7 = this;

        var deferred = defer();

        if (!this.options.ns) {
          callback && callback();
          return Promise.resolve();
        }

        if (typeof ns === 'string') ns = [ns];
        ns.forEach(function (n) {
          if (_this7.options.ns.indexOf(n) < 0) _this7.options.ns.push(n);
        });
        this.loadResources(function (err) {
          deferred.resolve();
          if (callback) callback(err);
        });
        return deferred;
      }
    }, {
      key: "loadLanguages",
      value: function loadLanguages(lngs, callback) {
        var deferred = defer();
        if (typeof lngs === 'string') lngs = [lngs];
        var preloaded = this.options.preload || [];
        var newLngs = lngs.filter(function (lng) {
          return preloaded.indexOf(lng) < 0;
        });

        if (!newLngs.length) {
          if (callback) callback();
          return Promise.resolve();
        }

        this.options.preload = preloaded.concat(newLngs);
        this.loadResources(function (err) {
          deferred.resolve();
          if (callback) callback(err);
        });
        return deferred;
      }
    }, {
      key: "dir",
      value: function dir(lng) {
        if (!lng) lng = this.languages && this.languages.length > 0 ? this.languages[0] : this.language;
        if (!lng) return 'rtl';
        var rtlLngs = ['ar', 'shu', 'sqr', 'ssh', 'xaa', 'yhd', 'yud', 'aao', 'abh', 'abv', 'acm', 'acq', 'acw', 'acx', 'acy', 'adf', 'ads', 'aeb', 'aec', 'afb', 'ajp', 'apc', 'apd', 'arb', 'arq', 'ars', 'ary', 'arz', 'auz', 'avl', 'ayh', 'ayl', 'ayn', 'ayp', 'bbz', 'pga', 'he', 'iw', 'ps', 'pbt', 'pbu', 'pst', 'prp', 'prd', 'ug', 'ur', 'ydd', 'yds', 'yih', 'ji', 'yi', 'hbo', 'men', 'xmn', 'fa', 'jpr', 'peo', 'pes', 'prs', 'dv', 'sam'];
        return rtlLngs.indexOf(this.services.languageUtils.getLanguagePartFromCode(lng)) >= 0 ? 'rtl' : 'ltr';
      }
    }, {
      key: "createInstance",
      value: function createInstance() {
        var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
        var callback = arguments.length > 1 ? arguments[1] : undefined;
        return new I18n(options, callback);
      }
    }, {
      key: "cloneInstance",
      value: function cloneInstance() {
        var _this8 = this;

        var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
        var callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop;

        var mergedOptions = _objectSpread({}, this.options, options, {
          isClone: true
        });

        var clone = new I18n(mergedOptions);
        var membersToCopy = ['store', 'services', 'language'];
        membersToCopy.forEach(function (m) {
          clone[m] = _this8[m];
        });
        clone.services = _objectSpread({}, this.services);
        clone.services.utils = {
          hasLoadedNamespace: clone.hasLoadedNamespace.bind(clone)
        };
        clone.translator = new Translator(clone.services, clone.options);
        clone.translator.on('*', function (event) {
          for (var _len4 = arguments.length, args = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {
            args[_key4 - 1] = arguments[_key4];
          }

          clone.emit.apply(clone, [event].concat(args));
        });
        clone.init(mergedOptions, callback);
        clone.translator.options = clone.options;
        clone.translator.backendConnector.services.utils = {
          hasLoadedNamespace: clone.hasLoadedNamespace.bind(clone)
        };
        return clone;
      }
    }]);

    return I18n;
  }(EventEmitter);

  var i18next = new I18n();

  var arr = [];
  var each = arr.forEach;
  var slice = arr.slice;
  function defaults(obj) {
    each.call(slice.call(arguments, 1), function (source) {
      if (source) {
        for (var prop in source) {
          if (obj[prop] === undefined) obj[prop] = source[prop];
        }
      }
    });
    return obj;
  }

  var cookie = {
    create: function create(name, value, minutes, domain) {
      var cookieOptions = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {
        path: '/'
      };
      var expires;

      if (minutes) {
        var date = new Date();
        date.setTime(date.getTime() + minutes * 60 * 1000);
        expires = '; expires=' + date.toUTCString();
      } else expires = '';

      domain = domain ? 'domain=' + domain + ';' : '';
      cookieOptions = Object.keys(cookieOptions).reduce(function (acc, key) {
        return acc + ';' + key.replace(/([A-Z])/g, function ($1) {
          return '-' + $1.toLowerCase();
        }) + '=' + cookieOptions[key];
      }, '');
      document.cookie = name + '=' + encodeURIComponent(value) + expires + ';' + domain + cookieOptions;
    },
    read: function read(name) {
      var nameEQ = name + '=';
      var ca = document.cookie.split(';');

      for (var i = 0; i < ca.length; i++) {
        var c = ca[i];

        while (c.charAt(0) === ' ') {
          c = c.substring(1, c.length);
        }

        if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
      }

      return null;
    },
    remove: function remove(name) {
      this.create(name, '', -1);
    }
  };
  var cookie$1 = {
    name: 'cookie',
    lookup: function lookup(options) {
      var found;

      if (options.lookupCookie && typeof document !== 'undefined') {
        var c = cookie.read(options.lookupCookie);
        if (c) found = c;
      }

      return found;
    },
    cacheUserLanguage: function cacheUserLanguage(lng, options) {
      if (options.lookupCookie && typeof document !== 'undefined') {
        cookie.create(options.lookupCookie, lng, options.cookieMinutes, options.cookieDomain, options.cookieOptions);
      }
    }
  };

  var querystring = {
    name: 'querystring',
    lookup: function lookup(options) {
      var found;

      if (typeof window !== 'undefined') {
        var query = window.location.search.substring(1);
        var params = query.split('&');

        for (var i = 0; i < params.length; i++) {
          var pos = params[i].indexOf('=');

          if (pos > 0) {
            var key = params[i].substring(0, pos);

            if (key === options.lookupQuerystring) {
              found = params[i].substring(pos + 1);
            }
          }
        }
      }

      return found;
    }
  };

  var hasLocalStorageSupport;

  try {
    hasLocalStorageSupport = window !== 'undefined' && window.localStorage !== null;
    var testKey = 'i18next.translate.boo';
    window.localStorage.setItem(testKey, 'foo');
    window.localStorage.removeItem(testKey);
  } catch (e) {
    hasLocalStorageSupport = false;
  }

  var localStorage$1 = {
    name: 'localStorage',
    lookup: function lookup(options) {
      var found;

      if (options.lookupLocalStorage && hasLocalStorageSupport) {
        var lng = window.localStorage.getItem(options.lookupLocalStorage);
        if (lng) found = lng;
      }

      return found;
    },
    cacheUserLanguage: function cacheUserLanguage(lng, options) {
      if (options.lookupLocalStorage && hasLocalStorageSupport) {
        window.localStorage.setItem(options.lookupLocalStorage, lng);
      }
    }
  };

  var hasSessionStorageSupport;

  try {
    hasSessionStorageSupport = window !== 'undefined' && window.sessionStorage !== null;
    var testKey$1 = 'i18next.translate.boo';
    window.sessionStorage.setItem(testKey$1, 'foo');
    window.sessionStorage.removeItem(testKey$1);
  } catch (e) {
    hasSessionStorageSupport = false;
  }

  var sessionStorage = {
    name: 'sessionStorage',
    lookup: function lookup(options) {
      var found;

      if (options.lookupsessionStorage && hasSessionStorageSupport) {
        var lng = window.sessionStorage.getItem(options.lookupsessionStorage);
        if (lng) found = lng;
      }

      return found;
    },
    cacheUserLanguage: function cacheUserLanguage(lng, options) {
      if (options.lookupsessionStorage && hasSessionStorageSupport) {
        window.sessionStorage.setItem(options.lookupsessionStorage, lng);
      }
    }
  };

  var navigator$1 = {
    name: 'navigator',
    lookup: function lookup(options) {
      var found = [];

      if (typeof navigator !== 'undefined') {
        if (navigator.languages) {
          // chrome only; not an array, so can't use .push.apply instead of iterating
          for (var i = 0; i < navigator.languages.length; i++) {
            found.push(navigator.languages[i]);
          }
        }

        if (navigator.userLanguage) {
          found.push(navigator.userLanguage);
        }

        if (navigator.language) {
          found.push(navigator.language);
        }
      }

      return found.length > 0 ? found : undefined;
    }
  };

  var htmlTag = {
    name: 'htmlTag',
    lookup: function lookup(options) {
      var found;
      var htmlTag = options.htmlTag || (typeof document !== 'undefined' ? document.documentElement : null);

      if (htmlTag && typeof htmlTag.getAttribute === 'function') {
        found = htmlTag.getAttribute('lang');
      }

      return found;
    }
  };

  var path = {
    name: 'path',
    lookup: function lookup(options) {
      var found;

      if (typeof window !== 'undefined') {
        var language = window.location.pathname.match(/\/([a-zA-Z-]*)/g);

        if (language instanceof Array) {
          if (typeof options.lookupFromPathIndex === 'number') {
            if (typeof language[options.lookupFromPathIndex] !== 'string') {
              return undefined;
            }

            found = language[options.lookupFromPathIndex].replace('/', '');
          } else {
            found = language[0].replace('/', '');
          }
        }
      }

      return found;
    }
  };

  var subdomain = {
    name: 'subdomain',
    lookup: function lookup(options) {
      var found;

      if (typeof window !== 'undefined') {
        var language = window.location.href.match(/(?:http[s]*\:\/\/)*(.*?)\.(?=[^\/]*\..{2,5})/gi);

        if (language instanceof Array) {
          if (typeof options.lookupFromSubdomainIndex === 'number') {
            found = language[options.lookupFromSubdomainIndex].replace('http://', '').replace('https://', '').replace('.', '');
          } else {
            found = language[0].replace('http://', '').replace('https://', '').replace('.', '');
          }
        }
      }

      return found;
    }
  };

  function getDefaults() {
    return {
      order: ['querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag'],
      lookupQuerystring: 'lng',
      lookupCookie: 'i18next',
      lookupLocalStorage: 'i18nextLng',
      // cache user language
      caches: ['localStorage'],
      excludeCacheFor: ['cimode'],
      //cookieMinutes: 10,
      //cookieDomain: 'myDomain'
      checkWhitelist: true,
      checkForSimilarInWhitelist: false
    };
  }

  var Browser =
  /*#__PURE__*/
  function () {
    function Browser(services) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

      _classCallCheck(this, Browser);

      this.type = 'languageDetector';
      this.detectors = {};
      this.init(services, options);
    }

    _createClass(Browser, [{
      key: "init",
      value: function init(services) {
        var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
        var i18nOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
        this.services = services;
        this.options = defaults(options, this.options || {}, getDefaults()); // if checking for similar, user needs to check whitelist

        if (this.options.checkForSimilarInWhitelist) this.options.checkWhitelist = true; // backwards compatibility

        if (this.options.lookupFromUrlIndex) this.options.lookupFromPathIndex = this.options.lookupFromUrlIndex;
        this.i18nOptions = i18nOptions;
        this.addDetector(cookie$1);
        this.addDetector(querystring);
        this.addDetector(localStorage$1);
        this.addDetector(sessionStorage);
        this.addDetector(navigator$1);
        this.addDetector(htmlTag);
        this.addDetector(path);
        this.addDetector(subdomain);
      }
    }, {
      key: "addDetector",
      value: function addDetector(detector) {
        this.detectors[detector.name] = detector;
      }
    }, {
      key: "detect",
      value: function detect(detectionOrder) {
        var _this = this;

        if (!detectionOrder) detectionOrder = this.options.order;
        var detected = [];
        detectionOrder.forEach(function (detectorName) {
          if (_this.detectors[detectorName]) {
            var lookup = _this.detectors[detectorName].lookup(_this.options);

            if (lookup && typeof lookup === 'string') lookup = [lookup];
            if (lookup) detected = detected.concat(lookup);
          }
        });
        var found;
        detected.forEach(function (lng) {
          if (found) return;

          var cleanedLng = _this.services.languageUtils.formatLanguageCode(lng);

          if (!_this.options.checkWhitelist || _this.services.languageUtils.isWhitelisted(cleanedLng)) found = cleanedLng;

          if (!found && _this.options.checkForSimilarInWhitelist) {
            found = _this.getSimilarInWhitelist(cleanedLng);
          }
        });

        if (!found) {
          var fallbacks = this.i18nOptions.fallbackLng;
          if (typeof fallbacks === 'string') fallbacks = [fallbacks];
          if (!fallbacks) fallbacks = [];

          if (Object.prototype.toString.apply(fallbacks) === '[object Array]') {
            found = fallbacks[0];
          } else {
            found = fallbacks[0] || fallbacks["default"] && fallbacks["default"][0];
          }
        }

        return found;
      }
    }, {
      key: "cacheUserLanguage",
      value: function cacheUserLanguage(lng, caches) {
        var _this2 = this;

        if (!caches) caches = this.options.caches;
        if (!caches) return;
        if (this.options.excludeCacheFor && this.options.excludeCacheFor.indexOf(lng) > -1) return;
        caches.forEach(function (cacheName) {
          if (_this2.detectors[cacheName]) _this2.detectors[cacheName].cacheUserLanguage(lng, _this2.options);
        });
      }
    }, {
      key: "getSimilarInWhitelist",
      value: function getSimilarInWhitelist(cleanedLng) {
        var _this3 = this;

        if (!this.i18nOptions.whitelist) return;

        if (cleanedLng.includes('-')) {
          // i.e. es-MX should check if es is in whitelist
          var prefix = cleanedLng.split('-')[0];
          var cleanedPrefix = this.services.languageUtils.formatLanguageCode(prefix);
          if (this.services.languageUtils.isWhitelisted(cleanedPrefix)) return cleanedPrefix; // if reached here, nothing found. continue to search for similar using only prefix

          cleanedLng = cleanedPrefix;
        } // i.e. 'pt' should return 'pt-BR'. If multiple in whitelist with 'pt-', then use first one in whitelist


        var similar = this.i18nOptions.whitelist.find(function (whitelistLng) {
          var cleanedWhitelistLng = _this3.services.languageUtils.formatLanguageCode(whitelistLng);

          if (cleanedWhitelistLng.startsWith(cleanedLng)) return cleanedWhitelistLng;
        });
        if (similar) return similar;
      }
    }]);

    return Browser;
  }();

  Browser.type = 'languageDetector';

  var fes = {
  	autoplay: "The media that tried to play (with '{{src}}') wasn't allowed to by this browser, most likely due to the browser's autoplay policy.\n\n+ More info: {{url}}",
  	checkUserDefinedFns: "It seems that you may have accidentally written {{name}} instead of {{actualName}}. Please correct it if it's not intentional.",
  	fileLoadError: {
  		bytes: "It looks like there was a problem loading your file. {{suggestion}}",
  		font: "It looks like there was a problem loading your font. {{suggestion}}",
  		gif: "There was some trouble loading your GIF. Make sure that your GIF is using 87a or 89a encoding.",
  		image: "It looks like there was a problem loading your image. {{suggestion}}",
  		json: "It looks like there was a problem loading your JSON file. {{suggestion}}",
  		large: "If your large file isn't fetched successfully, we recommend splitting the file into smaller segments and fetching those.",
  		strings: "It looks like there was a problem loading your text file. {{suggestion}}",
  		suggestion: "Try checking if the file path ({{filePath}}) is correct, hosting the file online, or running a local server.\n\n+ More info: {{url}}",
  		table: "It looks like there was a problem loading your table file. {{suggestion}}",
  		xml: "It looks like there was a problem loading your XML file. {{suggestion}}"
  	},
  	friendlyParamError: {
  		type_EMPTY_VAR: "{{location}} {{func}}() was expecting {{formatType}} for the {{position}} parameter, received an empty variable instead. If not intentional, this is often a problem with scope.\n\n+ More info: {{url}}",
  		type_TOO_FEW_ARGUMENTS: "{{location}} {{func}}() was expecting at least {{minParams}} arguments, but received only {{argCount}}.",
  		type_TOO_MANY_ARGUMENTS: "{{location}} {{func}}() was expecting no more than {{maxParams}} arguments, but received {{argCount}}.",
  		type_WRONG_TYPE: "{{location}} {{func}}() was expecting {{formatType}} for the {{position}} parameter, received {{argType}} instead."
  	},
  	globalErrors: {
  		reference: {
  			cannotAccess: "\n{{location}} \"{{symbol}}\" is used before declaration. Make sure you have declared the variable before using it.\n\n+ More info: {{url}}",
  			notDefined: "\n{{location}} \"{{symbol}}\" is not defined in the current scope. If you have defined it in your code, you should check its scope, spelling, and letter-casing (JavaScript is case-sensitive).\n\n+ More info: {{url}}"
  		},
  		stackSubseq: "└[{{location}}] \n\t Called from line {{line}} in {{func}}()\n",
  		stackTop: "┌[{{location}}] \n\t Error at line {{line}} in {{func}}()\n",
  		syntax: {
  			badReturnOrYield: "\nSyntax Error - return lies outside of a function. Make sure you’re not missing any brackets, so that return lies inside a function.\n\n+ More info: {{url}}",
  			invalidToken: "\nSyntax Error - Found a symbol that JavaScript doesn't recognize or didn't expect at it's place.\n\n+ More info: {{url}}",
  			missingInitializer: "\nSyntax Error - A const variable is declared but not initialized. In JavaScript, an initializer for a const is required. A value must be specified in the same statement in which the variable is declared. Check the line number in the error and assign the const variable a value.\n\n+ More info: {{url}}",
  			redeclaredVariable: "\nSyntax Error - \"{{symbol}}\" is being redeclared. JavaScript doesn't allow declaring a variable more than once. Check the line number in error for redeclaration of the variable.\n\n+ More info: {{url}}",
  			unexpectedToken: "\nSyntax Error - Symbol present at a place that wasn't expected.\nUsually this is due to a typo. Check the line number in the error for anything missing/extra.\n\n+ More info: {{url}}"
  		},
  		type: {
  			constAssign: "\n{{location}} A const variable is being re-assigned. In javascript, re-assigning a value to a constant is not allowed. If you want to re-assign new values to a variable, make sure it is declared as var or let.\n\n+ More info: {{url}}",
  			notfunc: "\n{{location}} \"{{symbol}}\" could not be called as a function.\nCheck the spelling, letter-casing (JavaScript is case-sensitive) and its type.\n\n+ More info: {{url}}",
  			notfuncObj: "\n{{location}} \"{{symbol}}\" could not be called as a function.\nVerify whether \"{{obj}}\" has \"{{symbol}}\" in it and check the spelling, letter-casing (JavaScript is case-sensitive) and its type.\n\n+ More info: {{url}}",
  			readFromNull: "\n{{location}} The property of null can't be read. In javascript the value null indicates that an object has no value.\n\n+ More info: {{url}}",
  			readFromUndefined: "\n{{location}} Cannot read property of undefined. Check the line number in error and make sure the variable which is being operated is not undefined.\n\n + More info: {{url}}"
  		}
  	},
  	libraryError: "{{location}} An error with message \"{{error}}\" occurred inside the p5js library when {{func}} was called. If not stated otherwise, it might be an issue with the arguments passed to {{func}}.",
  	location: "[{{file}}, line {{line}}]",
  	misspelling: "{{location}} It seems that you may have accidentally written \"{{name}}\" instead of \"{{actualName}}\". Please correct it to {{actualName}} if you wish to use the {{type}} from p5.js.",
  	misspelling_plural: "{{location}} It seems that you may have accidentally written \"{{name}}\".\nYou may have meant one of the following: \n{{suggestions}}",
  	misusedTopLevel: "Did you just try to use p5.js's {{symbolName}} {{symbolType}}? If so, you may want to move it into your sketch's setup() function.\n\n+ More info: {{url}}",
  	preloadDisabled: "The preload() function has been removed in p5.js 2.0. Please load assets in setup() using async / await keywords or callbacks instead. See https://github.com/processing/p5.js-compatibility for more information about 2.0 and compatibility, or https://dev.to/limzykenneth/asynchronous-p5js-20-458f for more information about promises and async/await.",
  	positions: {
  		p_1: "first",
  		p_10: "tenth",
  		p_11: "eleventh",
  		p_12: "twelfth",
  		p_2: "second",
  		p_3: "third",
  		p_4: "fourth",
  		p_5: "fifth",
  		p_6: "sixth",
  		p_7: "seventh",
  		p_8: "eighth",
  		p_9: "ninth"
  	},
  	pre: "\n🌸 p5.js says: {{message}}",
  	sketchReaderErrors: {
  		reservedConst: "you have used a p5.js reserved variable \"{{symbol}}\" make sure you change the variable name to something else.\n\n+ More info: {{url}}",
  		reservedFunc: "you have used a p5.js reserved function \"{{symbol}}\" make sure you change the function name to something else.\n\n+ More info: {{url}}"
  	},
  	welcome: "Welcome! This is your friendly debugger. To turn me off, switch to using p5.min.js.",
  	wrongPreload: "{{location}} An error with message \"{{error}}\" occurred inside the p5js library when \"{{func}}\" was called. If not stated otherwise, it might be due to \"{{func}}\" being called from preload. Nothing besides load calls (loadImage, loadJSON, loadFont, loadStrings, etc.) should be inside the preload function."
  };
  var en = {
  	fes: fes
  };

  // Only one language is imported above. This is intentional as other languages
  // will be hosted online and then downloaded whenever needed

  /*
   * Here, we define a default/fallback language which we can use without internet.
   * You won't have to change this when adding a new language.
   *
   * `translation` is the namespace we are using for our initial set of strings
   */
  var fallbackResources = {
    en: {
      translation: en
    }
  };

  /*
   * This is a list of languages that we have added so far.
   * If you have just added a new language (yay!), add its key to the list below
   * (`en` is english, `es` es español). Also add its export to
   * dev.js, which is another file in this folder.
   * @private
   */
  const languages = [
    'en',
    'es',
    'ko',
    'zh',
    'hi',
    'ja'
  ];

  if (typeof IS_MINIFIED === 'undefined') {
    // internationalization is only for the unminified build

    if (typeof P5_DEV_BUILD !== 'undefined') {
      // When the library is built in development mode ( using npm run dev )
      // we want to use the current translation files on the disk, which may have
      // been updated but not yet pushed to the CDN.
      let completeResources = require('../../translations/dev');
      for (const language of Object.keys(completeResources)) {
        // In es_translation, language is es and namespace is translation
        // In es_MX_translation, language is es-MX and namespace is translation
        const parts = language.split('_');
        const lng = parts.slice(0, parts.length - 1).join('-');
        const ns = parts[parts.length - 1];

        fallbackResources[lng] = fallbackResources[lng] || {};
        fallbackResources[lng][ns] = completeResources[language];
      }
    }
  }

  /*
   * This is our i18next "backend" plugin. It tries to fetch languages
   * from a CDN.
   * @private
   */
  class FetchResources {
    constructor(services, options) {
      this.init(services, options);
    }

    // run fetch with a timeout. Automatically rejects on timeout
    // default timeout = 2000 ms
    fetchWithTimeout(url, options, timeout = 2000) {
      return Promise.race([
        fetch(url, options),
        new Promise((_, reject) =>
          setTimeout(() => reject(new Error('timeout')), timeout)
        )
      ]);
    }

    init(services, options = {}) {
      this.services = services;
      this.options = options;
    }

    read(language, namespace, callback) {
      const loadPath = this.options.loadPath;

      if (language === this.options.fallback) {
        // if the default language of the user is the same as our inbuilt fallback,
        // there's no need to fetch resources from the cdn. This won't actually
        // need to run when we use "partialBundledLanguages" in the init
        // function.
        callback(null, fallbackResources[language][namespace]);
      } else if (languages.includes(language)) {
        // The user's language is included in the list of languages
        // that we so far added translations for.

        const url = this.services.interpolator.interpolate(loadPath, {
          lng: language,
          ns: namespace
        });
        this.loadUrl(url, callback);
      } else {
        // We don't have translations for this language. i18next will use
        // the default language instead.
        callback('Not found', false);
      }
    }

    loadUrl(url, callback) {
      this.fetchWithTimeout(url)
        .then(
          response => {
            const ok = response.ok;

            if (!ok) {
              // caught in the catch() below
              throw new Error(`failed loading ${url}`);
            }
            return response.json();
          },
          () => {
            // caught in the catch() below
            throw new Error(`failed loading ${url}`);
          }
        )
        .then(data => {
          return callback(null, data);
        })
        .catch(callback);
    }
  }
  FetchResources.type = 'backend';

  /**
   * This is our translation function. Give it a key and
   * it will retrieve the appropriate string
   * (within supported languages) according to the
   * user's browser's language settings.
   * @function translator
   * @param {String} key a key that corresponds to a message in our translation files
   * @param {Object} values values for use in the message under the given `key`
   * @returns {String} message (with values inserted) in the user's browser language
   * @private
   */
  let translator = (key, values) => {
    console.debug('p5.js translator called before translations were loaded');

    // Certain FES functionality may trigger before translations are downloaded.
    // Using "partialBundledLanguages" option during initialization, we can
    // still use our fallback language to display messages
    i18next.t(key, values); /* i18next-extract-disable-line */
  };
  // (We'll set this to a real value in the init function below!)

  /*
   * Set up our translation function, with loaded languages
   * @private
   */
  const initialize = () => {
    let i18init = i18next
      .use(Browser)
      .use(FetchResources)
      .init({
        fallbackLng: 'en',
        nestingPrefix: '$tr(',
        nestingSuffix: ')',
        defaultNS: 'translation',
        returnEmptyString: false,
        interpolation: {
          escapeValue: false
        },
        detection: {
          checkWhitelist: false,

          // prevent storing or locating language from cookie or localStorage
          // more info on https://github.com/processing/p5.js/issues/4862
          order: ['querystring', 'navigator', 'htmlTag', 'path', 'subdomain'],
          caches: []
        },
        backend: {
          fallback: 'en',
          
          // ensure that the FES internationalization strings are loaded
          // from the latest patch of the current minor version of p5.js
          loadPath: `https://cdn.jsdelivr.net/npm/p5@${
          VERSION.replace(/^(\d+\.\d+)\.\d+.*$/, '$1')
        }/translations/{{lng}}/{{ns}}.json`
        },
        partialBundledLanguages: true,
        resources: fallbackResources
      })
      .then(
        translateFn => {
          translator = translateFn;
        },
        e => console.debug(`Translations failed to load (${e})`)
      );

    // i18next.init() returns a promise that resolves when the translations
    // are loaded. We use this in core/init.js to hold p5 initialization until
    // we have the translation files.
    return i18init;
  };

  // This contains a data table used by ./fes_core.js/fesErrorMonitor().
  //
  // Note: Different browsers use different error strings for the same error.
  // Extracting info from the browser error messages is easier and cleaner
  // if we have a predefined lookup. This file serves as that lookup.
  // Using this lookup we match the errors obtained from the browser, classify
  // them into types and extract the required information.
  // The FES can use the extracted info to generate a friendly error message
  // for the matching error.
  const strings$1 = {
    ReferenceError: [
      {
        msg: '{{}} is not defined',
        type: 'NOTDEFINED',
        browser: 'all'
      },
      {
        msg: "Can't find variable: {{}}",
        type: 'NOTDEFINED',
        browser: 'Safari'
      },
      {
        msg: "Cannot access '{{.}}' before initialization",
        type: 'CANNOTACCESS',
        browser: 'Chrome'
      },
      {
        msg: "can't access lexical declaration '{{.}}' before initialization",
        type: 'CANNOTACCESS',
        browser: 'Firefox'
      }
    ],
    SyntaxError: [
      {
        msg: 'illegal character',
        type: 'INVALIDTOKEN',
        browser: 'Firefox'
      },
      {
        msg: 'Invalid character',
        type: 'INVALIDTOKEN',
        browser: 'Safari'
      },
      {
        msg: 'Invalid or unexpected token',
        type: 'INVALIDTOKEN',
        browser: 'Chrome'
      },
      {
        msg: "Unexpected token '{{.}}'",
        type: 'UNEXPECTEDTOKEN',
        browser: 'Chrome'
      },
      {
        msg: "expected {{.}}, got '{{.}}'",
        type: 'UNEXPECTEDTOKEN',
        browser: 'Chrome'
      },
      {
        msg: "Identifier '{{.}}' has already been declared",
        type: 'REDECLAREDVARIABLE',
        browser: 'Chrome'
      },
      {
        msg: 'redeclaration of {} {{.}}',
        type: 'REDECLAREDVARIABLE',
        browser: 'Firefox'
      },
      {
        msg: 'Missing initializer in const declaration',
        type: 'MISSINGINITIALIZER',
        browser: 'Chrome'
      },
      {
        msg: 'missing = in const declaration',
        type: 'MISSINGINITIALIZER',
        browser: 'Firefox'
      },
      {
        msg: 'Illegal return statement',
        type: 'BADRETURNORYIELD',
        browser: 'Chrome'
      },
      {
        msg: 'return not in function',
        type: 'BADRETURNORYIELD',
        browser: 'Firefox'
      }
    ],
    TypeError: [
      {
        msg: '{{.}} is not a function',
        type: 'NOTFUNC',
        browser: 'all'
      },
      {
        msg: 'Cannot read {{.}} null',
        type: 'READNULL',
        browser: 'Chrome'
      },
      {
        msg: '{{.}} is null',
        type: 'READNULL',
        browser: 'Firefox'
      },
      {
        msg: 'Cannot read {{.}} undefined',
        type: 'READUDEFINED',
        browser: 'Chrome'
      },
      {
        msg: '{{.}} is undefined',
        type: 'READUDEFINED',
        browser: 'Firefox'
      },
      {
        msg: 'Assignment to constant variable',
        type: 'CONSTASSIGN',
        browser: 'Chrome'
      },
      {
        msg: "invalid assignment to const '{{.}}'",
        type: 'CONSTASSIGN',
        browser: 'Firefox'
      }
    ]
  };

  /**
   * @for p5
   * @requires core
   *
   * This is the main file for the Friendly Error System (FES), containing
   * the core as well as miscellaneous functionality of the FES. Here is a
   * brief outline of the functions called in this system.
   *
   * The FES may be invoked by a call to either
   * (1) _validateParameters, (2) _friendlyFileLoadError, (3) _friendlyError,
   * (4) helpForMisusedAtTopLevelCode, or (5) _fesErrorMonitor.
   *
   * _validateParameters is located in validate_params.js along with other code
   * used for parameter validation.
   * _friendlyFileLoadError is located in file_errors.js along with other code
   * used for dealing with file load errors.
   * Apart from this, there's also a file stacktrace.js, which contains the code
   * to parse the error stack, borrowed from:
   * https://github.com/stacktracejs/stacktrace.js
   *
   * For more detailed information on the FES functions, including the call
   * sequence of each function, please look at the FES Reference + Dev Notes:
   * https://github.com/processing/p5.js/blob/main/contributor_docs/fes_reference_dev_notes.md
   */

  function fesCore(p5, fn){
    // p5.js blue, p5.js orange, auto dark green; fallback p5.js darkened magenta
    // See testColors below for all the color codes and names
    const typeColors = ['#2D7BB6', '#EE9900', '#4DB200', '#C83C00'];
    let misusedAtTopLevelCode = null;
    let defineMisusedAtTopLevelCode = null;

    // the threshold for the maximum allowed levenshtein distance
    // used in misspelling detection
    const EDIT_DIST_THRESHOLD = 2;

    // Used for internally thrown errors that should not get wrapped by another
    // friendly error handler
    class FESError extends Error {}
    if (typeof IS_MINIFIED !== 'undefined') {
      p5._friendlyError =
        p5._checkForUserDefinedFunctions =
        p5._fesErrorMonitor =
        () => {};
    } else {

      // const errorTable = require('./browser_errors').default;

      // -- Borrowed from jQuery 1.11.3 --
      const class2type = {};
      const toString = class2type.toString;
      const names = [
        'Boolean',
        'Number',
        'String',
        'Function',
        'Array',
        'Date',
        'RegExp',
        'Object',
        'Error'
      ];
      for (let n = 0; n < names.length; n++) {
        class2type[`[object ${names[n]}]`] = names[n].toLowerCase();
      }
      const getType = obj => {
        if (obj == null) {
          return `${obj}`;
        }
        return typeof obj === 'object' || typeof obj === 'function'
          ? class2type[toString.call(obj)] || 'object'
          : typeof obj;
      };

      // -- End borrow --

      // entry points into user-defined code
      const entryPoints = [
        'setup',
        'draw',
        'deviceMoved',
        'deviceTurned',
        'deviceShaken',
        'doubleClicked',
        'mousePressed',
        'mouseReleased',
        'mouseMoved',
        'mouseDragged',
        'mouseClicked',
        'mouseWheel',
        'touchStarted',
        'touchMoved',
        'touchEnded',
        'keyPressed',
        'keyReleased',
        'keyTyped',
        'windowResized'
      ];

      /**
       * Takes a message and a p5 function func, and adds a link pointing to
       * the reference documentation of func at the end of the message
       *
       * @method mapToReference
       * @private
       * @param {String}  message   the words to be said
       * @param {String}  [func]    the name of function
       *
       * @returns {String}
       */
      const mapToReference = (message, func) => {
        let msgWithReference = '';
        if (func == null || func.substring(0, 4) === 'load') {
          msgWithReference = message;
        } else {
          const methodParts = func.split('.');
          const referenceSection =
            methodParts.length > 1 ? `${methodParts[0]}.${methodParts[1]}` : 'p5';

          const funcName =
            methodParts.length === 1 ? func : methodParts.slice(2).join('/');

          //Whenever func having p5.[Class] is encountered, we need to have the error link as mentioned below else different link
          funcName.startsWith('p5.')  ?
            msgWithReference = `${message} (https://p5js.org/reference/${referenceSection}.${funcName})` :
            msgWithReference = `${message} (https://p5js.org/reference/${referenceSection}/${funcName})`;
        }
        return msgWithReference;
      };

      /**
       * Prints out a fancy, colorful message to the console log
       * Attaches Friendly Errors prefix [fes.pre] to the message.
       *
       * @method _report
       * @private
       * @param  {String}          message  Message to be printed
       * @param  {String}          [func]   Name of function
       * @param  {Number|String}   [color]  CSS color code
       *
       * @return console logs
       */
      p5._report = (message, func, color) => {
        // if p5._fesLogger is set ( i.e we are running tests ), use that
        // instead of console.log
        const log =
          p5._fesLogger == null ? console.log.bind(console) : p5._fesLogger;
        if ('undefined' === getType(color)) {
          color = '#B40033'; // dark magenta
        } else if (getType(color) === 'number') {
          // Type to color
          color = typeColors[color];
        }

        // Add a link to the reference docs of func at the end of the message
        message = mapToReference(message, func);
        const prefixedMsg = translator('fes.pre', { message });

        {
          log(prefixedMsg);
        }
      };

      /**
       * Throws an error with helpful p5 context. Similar to _report, but
       * this will stop other code execution to prevent downstream errors
       * from being logged.
       *
       * @method _error
       * @private
       * @param                    context  p5 instance the error is from
       * @param  {String}          message  Message to be printed
       * @param  {String}          [func]   Name of function
       */
      p5._error = (context, message, func) => {
        p5._report(message, func);
        context.hitCriticalError = true;
        // Throw an error to stop the current function (e.g. setup or draw) from
        // running more code
        throw new FESError('Stopping sketch to prevent more errors');
      };

      /**
       * This is a generic method that can be called from anywhere in the p5
       * library to alert users to a common error.
       *
       * @method _friendlyError
       * @private
       * @param  {String}         message   Message to be printed
       * @param  {String}         [func]    Name of the function linked to error
       * @param  {Number|String}  [color]   CSS color code
       */
      p5._friendlyError = function(message, func, color) {
        if (p5.disableFriendlyErrors) return;
        p5._report(message, func, color);
      };

      /**
       * This is called internally if there is an error with autoplay. Generates
       * and prints a friendly error message [fes.autoplay].
       *
       * @method _friendlyAutoplayError
       * @private
       */
      p5._friendlyAutoplayError = function(src) {
        const message = translator('fes.autoplay', {
          src,
          url: 'https://developer.mozilla.org/docs/Web/Media/Autoplay_guide'
        });
        console.log(translator('fes.pre', { message }));
      };

      /**
       * Measures dissimilarity between two strings by calculating
       * the Levenshtein distance.
       *
       * If the "distance" between them is small enough, it is
       * reasonable to think that one is the misspelled version of the other.
       *
       * Specifically, this uses the Wagner–Fischer algorithm.
       * @method computeEditDistance
       * @private
       * @param {String} w1 the first word
       * @param {String} w2 the second word
       *
       * @returns {Number} the "distance" between the two words, a smaller value
       *                   indicates that the words are similar
       */
      const computeEditDistance = (w1, w2) => {
        const l1 = w1.length,
          l2 = w2.length;
        if (l1 === 0) return w2;
        if (l2 === 0) return w1;

        let prev = [];
        let cur = [];

        for (let j = 0; j < l2 + 1; j++) {
          cur[j] = j;
        }

        prev = cur;

        for (let i = 1; i < l1 + 1; i++) {
          cur = [];
          for (let j = 0; j < l2 + 1; j++) {
            if (j === 0) {
              cur[j] = i;
            } else {
              let a1 = w1[i - 1],
                a2 = w2[j - 1];
              let temp = 999999;
              let cost = a1.toLowerCase() === a2.toLowerCase() ? 0 : 1;
              temp = temp > cost + prev[j - 1] ? cost + prev[j - 1] : temp;
              temp = temp > 1 + cur[j - 1] ? 1 + cur[j - 1] : temp;
              temp = temp > 1 + prev[j] ? 1 + prev[j] : temp;
              cur[j] = temp;
            }
          }
          prev = cur;
        }

        return cur[l2];
      };

      /**
       * Whether or not p5.js is running in an environment where `preload` will be
       * run before `setup`.
       *
       * This will return false for default builds >= 2.0, but backwards compatibility
       * addons may set this to true.
       *
       * @private
       */
      p5.isPreloadSupported = function() {
        return false;
      };

      /**
       * Checks capitalization for user defined functions.
       *
       * Generates and prints a friendly error message using key:
       * "fes.checkUserDefinedFns".
       *
       * @method checkForUserDefinedFunctions
       * @private
       * @param {*} context   Current default context. Set to window in
       *                      "global mode" and to a p5 instance in "instance mode"
       */
      const checkForUserDefinedFunctions = context => {
        if (p5.disableFriendlyErrors) return;

        // if using instance mode, this function would be called with the current
        // instance as context
        const instanceMode = context instanceof p5;
        context = instanceMode ? context : window;
        const fnNames = entryPoints;

        if (context.preload && !p5.isPreloadSupported()) {
          p5._error(context, translator('fes.preloadDisabled'));
        }

        const fxns = {};
        // lowercasename -> actualName mapping
        fnNames.forEach(symbol => {
          fxns[symbol.toLowerCase()] = symbol;
        });

        for (const prop of Object.keys(context)) {
          const lowercase = prop.toLowerCase();

          // check if the lowercase property name has an entry in fxns, if the
          // actual name with correct capitalization doesnt exist in context,
          // and if the user-defined symbol is of the type function
          if (
            fxns.hasOwnProperty(lowercase) &&
            !context[fxns[lowercase]] &&
            typeof context[prop] === 'function'
          ) {
            const msg = translator('fes.checkUserDefinedFns', {
              name: prop,
              actualName: fxns[lowercase]
            });

            p5._friendlyError(msg, fxns[lowercase]);
          }
        }
      };

      /**
       * Compares the symbol caught in the ReferenceError to everything in
       * misusedAtTopLevel ( all public p5 properties ).
       *
       * Generates and prints a friendly error message using key: "fes.misspelling".
       *
       * @method handleMisspelling
       * @private
       * @param {String} errSym   Symbol to whose spelling to check
       * @param {Error} error     ReferenceError object
       *
       * @returns {Boolean} tell whether error was likely due to typo
       */
      const handleMisspelling = (errSym, error) => {
        if (!misusedAtTopLevelCode) {
          defineMisusedAtTopLevelCode();
        }

        const distanceMap = {};
        let min = 999999;
        // compute the levenshtein distance for the symbol against all known
        // public p5 properties. Find the property with the minimum distance
        misusedAtTopLevelCode.forEach(symbol => {
          let dist = computeEditDistance(errSym, symbol.name);
          if (distanceMap[dist]) distanceMap[dist].push(symbol);
          else distanceMap[dist] = [symbol];

          if (dist < min) min = dist;
        });

        // if the closest match has more "distance" than the max allowed threshold
        if (min > Math.min(EDIT_DIST_THRESHOLD, errSym.length)) return false;

        // Show a message only if the caught symbol and the matched property name
        // differ in their name ( either letter difference or difference of case )
        const matchedSymbols = distanceMap[min].filter(
          symbol => symbol.name !== errSym
        );
        if (matchedSymbols.length !== 0) {
          const parsed = p5._getErrorStackParser().parse(error);
          let locationObj;
          if (
            parsed &&
            parsed[0] &&
            parsed[0].fileName &&
            parsed[0].lineNumber &&
            parsed[0].columnNumber
          ) {
            locationObj = {
              location: `${parsed[0].fileName}:${parsed[0].lineNumber}:${
              parsed[0].columnNumber
            }`,
              file: parsed[0].fileName.split('/').slice(-1),
              line: parsed[0].lineNumber
            };
          }

          let msg;
          if (matchedSymbols.length === 1) {
            // To be used when there is only one closest match. The count parameter
            // allows i18n to pick between the keys "fes.misspelling" and
            // "fes.misspelling_plural"
            msg = translator('fes.misspelling', {
              name: errSym,
              actualName: matchedSymbols[0].name,
              type: matchedSymbols[0].type,
              location: locationObj ? translator('fes.location', locationObj) : '',
              count: matchedSymbols.length
            });
          } else {
            // To be used when there are multiple closest matches. Gives each
            // suggestion on its own line, the function name followed by a link to
            // reference documentation
            const suggestions = matchedSymbols
              .map(symbol => {
                const message =
                  '▶️ ' + symbol.name + (symbol.type === 'function' ? '()' : '');
                return mapToReference(message, symbol.name);
              })
              .join('\n');

            msg = translator('fes.misspelling', {
              name: errSym,
              suggestions,
              location: locationObj ? translator('fes.location', locationObj) : '',
              count: matchedSymbols.length
            });
          }

          // If there is only one closest match, tell _friendlyError to also add
          // a link to the reference documentation. In case of multiple matches,
          // this is already done in the suggestions variable, one link for each
          // suggestion.
          p5._friendlyError(
            msg,
            matchedSymbols.length === 1 ? matchedSymbols[0].name : undefined
          );
          return true;
        }
        return false;
      };

      /**
       * Prints a friendly stacktrace for user-written functions for "global" errors
       *
       * Generates and prints a friendly error message using key:
       * "fes.globalErrors.stackTop", "fes.globalErrors.stackSubseq".
       *
       * @method printFriendlyStack
       * @private
       * @param {Array} friendlyStack
       */
      const printFriendlyStack = friendlyStack => {
        const log =
          p5._fesLogger && typeof p5._fesLogger === 'function'
            ? p5._fesLogger
            : console.log.bind(console);
        if (friendlyStack.length > 1) {
          let stacktraceMsg = '';
          friendlyStack.forEach((frame, idx) => {
            const location = `${frame.fileName}:${frame.lineNumber}:${
            frame.columnNumber
          }`;
            let frameMsg,
              translationObj = {
                func: frame.functionName,
                line: frame.lineNumber,
                location,
                file: frame.fileName.split('/').slice(-1)
              };
            if (idx === 0) {
              frameMsg = translator('fes.globalErrors.stackTop', translationObj);
            } else {
              frameMsg = translator('fes.globalErrors.stackSubseq', translationObj);
            }
            stacktraceMsg += frameMsg;
          });
          log(stacktraceMsg);
        }
      };

      /**
       * Takes a stacktrace array and filters out all frames that show internal p5
       * details.
       *
       * Generates and prints a friendly error message using key:
       * "fes.wrongPreload", "fes.libraryError".
       *
       * The processed stack is used to find whether the error happened internally
       * within the library, and if the error was due to a non-loadX() method
       * being used in preload.
       *
       * "Internally" here means that the exact location of the error (the top of
       * the stack) is a piece of code written in the p5.js library (which may or
       * may not have been called from the user's sketch).
       *
       * @method processStack
       * @private
       * @param {Error} error
       * @param {Array} stacktrace
       *
       * @returns {Array} An array with two elements, [isInternal, friendlyStack]
       *                 isInternal: a boolean value indicating whether the error
       *                             happened internally
       *                 friendlyStack: the filtered (simplified) stacktrace
       */
      const processStack = (error, stacktrace) => {
        // cannot process a stacktrace that doesn't exist
        if (!stacktrace) return [false, null];

        stacktrace.forEach(frame => {
          frame.functionName = frame.functionName || '';
        });

        // isInternal - Did this error happen inside the library
        let isInternal = false;
        let p5FileName, friendlyStack, currentEntryPoint;

        // Intentionally throw an error that we catch so that we can check the name
        // of the current file. Any errors we see from this file, we treat as
        // internal errors.
        try {
          throw new Error();
        } catch (testError) {
          const testStacktrace = p5._getErrorStackParser().parse(testError);
          p5FileName = testStacktrace[0].fileName;
        }

        for (let i = stacktrace.length - 1; i >= 0; i--) {
          let splitted = stacktrace[i].functionName.split('.');
          if (entryPoints.includes(splitted[splitted.length - 1])) {
            // remove everything below an entry point function (setup, draw, etc).
            // (it's usually the internal initialization calls)
            friendlyStack = stacktrace.slice(0, i + 1);
            currentEntryPoint = splitted[splitted.length - 1];
            // We call the error "internal" if the source of the error was a
            // function from within the p5.js library file, but called from the
            // user's code directly. We only need to check the topmost frame in
            // the stack trace since any function internal to p5 should pass this
            // check, not just public p5 functions.
            if (stacktrace[0].fileName === p5FileName) {
              isInternal = true;
              break;
            }
            break;
          }
        }

        // in some cases ( errors in promises, callbacks, etc), no entry-point
        // function may be found in the stacktrace. In that case just use the
        // entire stacktrace for friendlyStack
        if (!friendlyStack) friendlyStack = stacktrace;

        if (isInternal) {
          // the frameIndex property is added before the filter, so frameIndex
          // corresponds to the index of a frame in the original stacktrace.
          // Then we filter out all frames which belong to the file that contains
          // the p5 library
          friendlyStack = friendlyStack
            .map((frame, index) => {
              frame.frameIndex = index;
              return frame;
            })
            .filter(frame => frame.fileName !== p5FileName);

          // a weird case, if for some reason we can't identify the function called
          // from user's code
          if (friendlyStack.length === 0) return [true, null];

          // get the function just above the topmost frame in the friendlyStack.
          // i.e the name of the library function called from user's code
          const func = stacktrace[friendlyStack[0].frameIndex - 1].functionName
            .split('.')
            .slice(-1)[0];

          // Try and get the location (line no.) from the top element of the stack
          let locationObj;
          if (
            friendlyStack[0].fileName &&
            friendlyStack[0].lineNumber &&
            friendlyStack[0].columnNumber
          ) {
            locationObj = {
              location: `${friendlyStack[0].fileName}:${
              friendlyStack[0].lineNumber
            }:${friendlyStack[0].columnNumber}`,
              file: friendlyStack[0].fileName.split('/').slice(-1),
              line: friendlyStack[0].lineNumber
            };

            // if already handled by another part of the FES, don't handle again
            if (p5._fesLogCache[locationObj.location]) return [true, null];
          }

          // Check if the error is due to a non loadX method being used incorrectly
          // in preload
          if (
            currentEntryPoint === 'preload' &&
            fn._preloadMethods[func] == null
          ) {
            p5._friendlyError(
              translator('fes.wrongPreload', {
                func,
                location: locationObj
                  ? translator('fes.location', locationObj)
                  : '',
                error: error.message
              }),
              'preload'
            );
          } else {
            // Library error
            p5._friendlyError(
              translator('fes.libraryError', {
                func,
                location: locationObj
                  ? translator('fes.location', locationObj)
                  : '',
                error: error.message
              }),
              func
            );
          }

          // Finally, if it's an internal error, print the friendlyStack
          // ( fesErrorMonitor won't handle this error )
          if (friendlyStack && friendlyStack.length) {
            printFriendlyStack(friendlyStack);
          }
        }
        return [isInternal, friendlyStack];
      };

      /**
       * Handles "global" errors that the browser catches.
       *
       * Called when an error event happens and detects the type of error.
       *
       * Generates and prints a friendly error message using key:
       * "fes.globalErrors.syntax.[*]", "fes.globalErrors.reference.[*]",
       * "fes.globalErrors.type.[*]".
       *
       * @method fesErrorMonitor
       * @private
       * @param {*} e  Event object to extract error details from
       */
      const fesErrorMonitor = e => {
        if (p5.disableFriendlyErrors) return;

        // Don't try to handle an error intentionally emitted by FES to halt execution
        if (e && (e instanceof FESError || e.reason instanceof FESError)) return;

        // Try to get the error object from e
        let error;
        if (e instanceof Error) {
          error = e;
        } else if (e instanceof ErrorEvent) {
          error = e.error;
        } else if (e instanceof PromiseRejectionEvent) {
          error = e.reason;
          if (!(error instanceof Error)) return;
        }
        if (!error) return;

        let stacktrace = p5._getErrorStackParser().parse(error);
        // process the stacktrace from the browser and simplify it to give
        // friendlyStack.
        let [isInternal, friendlyStack] = processStack(error, stacktrace);

        // if this is an internal library error, the type of the error is not relevant,
        // only the user code that lead to it is.
        if (isInternal) {
          return;
        }

        const errList = strings$1[error.name];
        if (!errList) return; // this type of error can't be handled yet
        let matchedError;
        for (const obj of errList) {
          let string = obj.msg;
          // capture the primary symbol mentioned in the error
          string = string.replace(new RegExp('{{}}', 'g'), '([a-zA-Z0-9_]+)');
          string = string.replace(new RegExp('{{.}}', 'g'), '(.+)');
          string = string.replace(new RegExp('{}', 'g'), '(?:[a-zA-Z0-9_]+)');
          let matched = error.message.match(string);

          if (matched) {
            matchedError = Object.assign({}, obj);
            matchedError.match = matched;
            break;
          }
        }

        if (!matchedError) return;

        // Try and get the location from the top element of the stack
        let locationObj;
        if (
          stacktrace &&
          stacktrace[0].fileName &&
          stacktrace[0].lineNumber &&
          stacktrace[0].columnNumber
        ) {
          locationObj = {
            location: `${stacktrace[0].fileName}:${stacktrace[0].lineNumber}:${
            stacktrace[0].columnNumber
          }`,
            file: stacktrace[0].fileName.split('/').slice(-1),
            line: friendlyStack[0].lineNumber
          };
        }

        switch (error.name) {
          case 'SyntaxError': {
            // We can't really do much with syntax errors other than try to use
            // a simpler framing of the error message. The stack isn't available
            // for syntax errors
            switch (matchedError.type) {
              case 'INVALIDTOKEN': {
                //Error if there is an invalid or unexpected token that doesn't belong at this position in the code
                //let x = “not a string”; -> string not in proper quotes
                let url =
                  'https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Illegal_character#What_went_wrong';
                p5._friendlyError(
                  translator('fes.globalErrors.syntax.invalidToken', {
                    url
                  })
                );
                break;
              }
              case 'UNEXPECTEDTOKEN': {
                //Error if a specific language construct(, { ; etc) was expected, but something else was provided
                //for (let i = 0; i < 5,; ++i) -> a comma after i<5 instead of a semicolon
                let url =
                  'https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Unexpected_token#What_went_wrong';
                p5._friendlyError(
                  translator('fes.globalErrors.syntax.unexpectedToken', {
                    url
                  })
                );
                break;
              }
              case 'REDECLAREDVARIABLE': {
                //Error if a variable is redeclared by the user. Example=>
                //let a = 10;
                //let a = 100;
                let errSym = matchedError.match[1];
                let url =
                  'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Redeclared_parameter#what_went_wrong';
                p5._friendlyError(
                  translator('fes.globalErrors.syntax.redeclaredVariable', {
                    symbol: errSym,
                    url
                  })
                );
                break;
              }
              case 'MISSINGINITIALIZER': {
                //Error if a const variable is not initialized during declaration
                //Example => const a;
                let url =
                  'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Missing_initializer_in_const#what_went_wrong';
                p5._friendlyError(
                  translator('fes.globalErrors.syntax.missingInitializer', {
                    url
                  })
                );
                break;
              }
              case 'BADRETURNORYIELD': {
                //Error when a return statement is misplaced(usually outside of a function)
                // const a = function(){
                //  .....
                //  }
                //  return; -> misplaced return statement
                let url =
                  'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Bad_return_or_yield#what_went_wrong';
                p5._friendlyError(
                  translator('fes.globalErrors.syntax.badReturnOrYield', {
                    url
                  })
                );
                break;
              }
            }
            break;
          }
          case 'ReferenceError': {
            switch (matchedError.type) {
              case 'NOTDEFINED': {
                //Error if there is a non-existent variable referenced somewhere
                //let a = 10;
                //console.log(x);
                let errSym = matchedError.match[1];

                if (errSym && handleMisspelling(errSym, error)) {
                  break;
                }

                // if the flow gets this far, this is likely not a misspelling
                // of a p5 property/function
                let url = 'https://p5js.org/examples/data-variable-scope.html';
                p5._friendlyError(
                  translator('fes.globalErrors.reference.notDefined', {
                    url,
                    symbol: errSym,
                    location: locationObj
                      ? translator('fes.location', locationObj)
                      : ''
                  })
                );

                if (friendlyStack) printFriendlyStack(friendlyStack);
                break;
              }
              case 'CANNOTACCESS': {
                //Error if a lexical variable was accessed before it was initialized
                //console.log(a); -> variable accessed before it was initialized
                //let a=100;
                let errSym = matchedError.match[1];
                let url =
                  'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cant_access_lexical_declaration_before_init#what_went_wrong';
                p5._friendlyError(
                  translator('fes.globalErrors.reference.cannotAccess', {
                    url,
                    symbol: errSym,
                    location: locationObj
                      ? translator('fes.location', locationObj)
                      : ''
                  })
                );

                if (friendlyStack) printFriendlyStack(friendlyStack);
                break;
              }
            }
            break;
          }

          case 'TypeError': {
            switch (matchedError.type) {
              case 'NOTFUNC': {
                //Error when some code expects you to provide a function, but that didn't happen
                //let a = document.getElementByID('foo'); -> getElementById instead of getElementByID
                let errSym = matchedError.match[1];
                let splitSym = errSym.split('.');
                let url =
                  'https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_a_function#What_went_wrong';

                // if errSym is aa.bb.cc , symbol would be cc and obj would aa.bb
                let translationObj = {
                  url,
                  symbol: splitSym[splitSym.length - 1],
                  obj: splitSym.slice(0, splitSym.length - 1).join('.'),
                  location: locationObj
                    ? translator('fes.location', locationObj)
                    : ''
                };

                // There are two cases to handle here. When the function is called
                // as a property of an object and when it's called independently.
                // Both have different explanations.
                if (splitSym.length > 1) {
                  p5._friendlyError(
                    translator('fes.globalErrors.type.notfuncObj', translationObj)
                  );
                } else {
                  p5._friendlyError(
                    translator('fes.globalErrors.type.notfunc', translationObj)
                  );
                }

                if (friendlyStack) printFriendlyStack(friendlyStack);
                break;
              }
              case 'READNULL': {
                //Error if a property of null is accessed
                //let a = null;
                //console.log(a.property); -> a is null
                let errSym = matchedError.match[1];
                let url =
                  'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cant_access_property#what_went_wrong';
                /*let url2 =
                  'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/null';*/
                p5._friendlyError(
                  translator('fes.globalErrors.type.readFromNull', {
                    url,
                    symbol: errSym,
                    location: locationObj
                      ? translator('fes.location', locationObj)
                      : ''
                  })
                );

                if (friendlyStack) printFriendlyStack(friendlyStack);
                break;
              }
              case 'READUDEFINED': {
                //Error if a property of undefined is accessed
                //let a; -> default value of a is undefined
                //console.log(a.property); -> a is undefined
                let errSym = matchedError.match[1];
                let url =
                  'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cant_access_property#what_went_wrong';
                /*let url2 =
                  'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined#description';*/
                p5._friendlyError(
                  translator('fes.globalErrors.type.readFromUndefined', {
                    url,
                    symbol: errSym,
                    location: locationObj
                      ? translator('fes.location', locationObj)
                      : ''
                  })
                );

                if (friendlyStack) printFriendlyStack(friendlyStack);
                break;
              }
              case 'CONSTASSIGN': {
                //Error when a const variable is reassigned a value
                //const a = 100;
                //a=10;
                let url =
                  'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_const_assignment#what_went_wrong';
                p5._friendlyError(
                  translator('fes.globalErrors.type.constAssign', {
                    url,
                    location: locationObj
                      ? translator('fes.location', locationObj)
                      : ''
                  })
                );

                if (friendlyStack) printFriendlyStack(friendlyStack);
                break;
              }
            }
          }
        }
      };

      p5._fesErrorMonitor = fesErrorMonitor;
      p5._checkForUserDefinedFunctions = checkForUserDefinedFunctions;

      // logger for testing purposes.
      p5._fesLogger = null;
      p5._fesLogCache = {};

      window.addEventListener('load', checkForUserDefinedFunctions, false);
      window.addEventListener('error', p5._fesErrorMonitor, false);
      window.addEventListener('unhandledrejection', p5._fesErrorMonitor, false);

      /**
       * Prints out all the colors in the color pallete with white text.
       * For color blindness testing.
       */
      /* function testColors() {
        const str = 'A box of biscuits, a box of mixed biscuits and a biscuit mixer';
        p5._friendlyError(str, 'print', '#ED225D'); // p5.js magenta
        p5._friendlyError(str, 'print', '#2D7BB6'); // p5.js blue
        p5._friendlyError(str, 'print', '#EE9900'); // p5.js orange
        p5._friendlyError(str, 'print', '#A67F59'); // p5.js light brown
        p5._friendlyError(str, 'print', '#704F21'); // p5.js gold
        p5._friendlyError(str, 'print', '#1CC581'); // auto cyan
        p5._friendlyError(str, 'print', '#FF6625'); // auto orange
        p5._friendlyError(str, 'print', '#79EB22'); // auto green
        p5._friendlyError(str, 'print', '#B40033'); // p5.js darkened magenta
        p5._friendlyError(str, 'print', '#084B7F'); // p5.js darkened blue
        p5._friendlyError(str, 'print', '#945F00'); // p5.js darkened orange
        p5._friendlyError(str, 'print', '#6B441D'); // p5.js darkened brown
        p5._friendlyError(str, 'print', '#2E1B00'); // p5.js darkened gold
        p5._friendlyError(str, 'print', '#008851'); // auto dark cyan
        p5._friendlyError(str, 'print', '#C83C00'); // auto dark orange
        p5._friendlyError(str, 'print', '#4DB200'); // auto dark green
      } */
    }

    // This is a lazily-defined list of p5 symbols that may be
    // misused by beginners at top-level code, outside of setup/draw. We'd like
    // to detect these errors and help the user by suggesting they move them
    // into setup/draw.
    //
    // For more details, see https://github.com/processing/p5.js/issues/1121.
    misusedAtTopLevelCode = null;
    const FAQ_URL =
      'https://github.com/processing/p5.js/wiki/p5.js-overview#why-cant-i-assign-variables-using-p5-functions-and-variables-before-setup';

    /**
     * A helper function for populating misusedAtTopLevel list.
     *
     * @method defineMisusedAtTopLevelCode
     * @private
     */
    defineMisusedAtTopLevelCode = () => {
      const uniqueNamesFound = {};

      const getSymbols = obj =>
        Object.getOwnPropertyNames(obj)
          .filter(name => {
            if (name[0] === '_') {
              return false;
            }
            if (name in uniqueNamesFound) {
              return false;
            }

            uniqueNamesFound[name] = true;

            return true;
          })
          .map(name => {
            let type;

            if (typeof obj[name] === 'function') {
              type = 'function';
            } else if (name === name.toUpperCase()) {
              type = 'constant';
            } else {
              type = 'variable';
            }

            return { name, type };
          });

      misusedAtTopLevelCode = [].concat(
        getSymbols(fn),
        // At present, p5 only adds its constants to fn during
        // construction, which may not have happened at the time a
        // ReferenceError is thrown, so we'll manually add them to our list.
        getSymbols(constants)
      );

      // This will ultimately ensure that we report the most specific error
      // possible to the user, e.g. advising them about HALF_PI instead of PI
      // when their code misuses the former.
      misusedAtTopLevelCode.sort((a, b) => b.name.length - a.name.length);
    };

    /**
     * Detects browser level error event for p5 constants/functions used outside
     * of setup() and draw().
     *
     * Generates and prints a friendly error message using key:
     * "fes.misusedTopLevel".
     *
     * @method helpForMisusedAtTopLevelCode
     * @private
     * @param {Event} e       Error event
     * @param {Boolean} log   false
     *
     * @returns {Boolean} true
     */
    const helpForMisusedAtTopLevelCode = (e, log) => {
      if (!log) {
        log = console.log.bind(console);
      }

      if (!misusedAtTopLevelCode) {
        defineMisusedAtTopLevelCode();
      }

      // If we find that we're logging lots of false positives, we can
      // uncomment the following code to avoid displaying anything if the
      // user's code isn't likely to be using p5's global mode. (Note that
      // setup/draw are more likely to be defined due to JS function hoisting.)
      //
      //if (!('setup' in window || 'draw' in window)) {
      //  return;
      //}

      misusedAtTopLevelCode.some(symbol => {
        // Note that while just checking for the occurrence of the
        // symbol name in the error message could result in false positives,
        // a more rigorous test is difficult because different browsers
        // log different messages, and the format of those messages may
        // change over time.
        //
        // For example, if the user uses 'PI' in their code, it may result
        // in any one of the following messages:
        //
        //   * 'PI' is undefined                           (Microsoft Edge)
        //   * ReferenceError: PI is undefined             (Firefox)
        //   * Uncaught ReferenceError: PI is not defined  (Chrome)

        if (e.message && e.message.match(`\\W?${symbol.name}\\W`) !== null) {
          const symbolName =
            symbol.type === 'function' ? `${symbol.name}()` : symbol.name;
          if (typeof IS_MINIFIED !== 'undefined') {
            log(
              `Did you just try to use p5.js's ${symbolName} ${
              symbol.type
            }? If so, you may want to move it into your sketch's setup() function.\n\nFor more details, see: ${FAQ_URL}`
            );
          } else {
            log(
              translator('fes.misusedTopLevel', {
                symbolName,
                symbolType: symbol.type,
                url: FAQ_URL
              })
            );
          }
          return true;
        }
      });
    };

    // Exposing this primarily for unit testing.
    fn._helpForMisusedAtTopLevelCode = helpForMisusedAtTopLevelCode;

    if (document.readyState !== 'complete') {
      window.addEventListener('error', helpForMisusedAtTopLevelCode, false);

      // Our job is only to catch ReferenceErrors that are thrown when
      // global (non-instance mode) p5 APIs are used at the top-level
      // scope of a file, so we'll unbind our error listener now to make
      // sure we don't log false positives later.
      window.addEventListener('load', () => {
        window.removeEventListener('error', helpForMisusedAtTopLevelCode, false);
      });
    }
  }

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

  /**
   * @for p5
   * @requires core
   */
  // Borrow from stacktracejs https://github.com/stacktracejs/stacktrace.js with
  // minor modifications. The license for the same and the code is included below

  // Copyright (c) 2017 Eric Wendelin and other contributors
  // 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.
  function ErrorStackParser() {
    let FIREFOX_SAFARI_STACK_REGEXP = /(^|@)\S+:\d+/;
    let CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
    let SAFARI_NATIVE_CODE_REGEXP = /^(eval@)?(\[native code])?$/;

    return {
      /**
       * Given an Error object, extract the most information from it.
       * @private
       * @param {Error} error object
       * @return {Array} of stack frames
       */
      parse: function ErrorStackParser$$parse(error) {
        if (
          typeof error.stacktrace !== 'undefined' ||
          typeof error['opera#sourceloc'] !== 'undefined'
        ) {
          return this.parseOpera(error);
        } else if (error.stack && error.stack.match(CHROME_IE_STACK_REGEXP)) {
          return this.parseV8OrIE(error);
        } else if (error.stack) {
          return this.parseFFOrSafari(error);
        } else ;
      },

      // Separate line and column numbers from a string of the form: (URI:Line:Column)
      extractLocation: function ErrorStackParser$$extractLocation(urlLike) {
        // Fail-fast but return locations like "(native)"
        if (urlLike.indexOf(':') === -1) {
          return [urlLike];
        }

        let regExp = /(.+?)(?::(\d+))?(?::(\d+))?$/;
        let parts = regExp.exec(urlLike.replace(/[()]/g, ''));
        return [parts[1], parts[2] || undefined, parts[3] || undefined];
      },

      parseV8OrIE: function ErrorStackParser$$parseV8OrIE(error) {
        let filtered = error.stack.split('\n').filter(function(line) {
          return !!line.match(CHROME_IE_STACK_REGEXP);
        }, this);

        return filtered.map(function(line) {
          if (line.indexOf('(eval ') > -1) {
            // Throw away eval information until we implement stacktrace.js/stackframe#8
            line = line
              .replace(/eval code/g, 'eval')
              .replace(/(\(eval at [^()]*)|(\),.*$)/g, '');
          }
          let sanitizedLine = line
            .replace(/^\s+/, '')
            .replace(/\(eval code/g, '(');

          // capture and preseve the parenthesized location "(/foo/my bar.js:12:87)" in
          // case it has spaces in it, as the string is split on \s+ later on
          let location = sanitizedLine.match(/ (\((.+):(\d+):(\d+)\)$)/);

          // remove the parenthesized location from the line, if it was matched
          sanitizedLine = location
            ? sanitizedLine.replace(location[0], '')
            : sanitizedLine;

          let tokens = sanitizedLine.split(/\s+/).slice(1);
          // if a location was matched, pass it to extractLocation() otherwise pop the last token
          let locationParts = this.extractLocation(
            location ? location[1] : tokens.pop()
          );
          let functionName = tokens.join(' ') || undefined;
          let fileName =
            ['eval', '<anonymous>'].indexOf(locationParts[0]) > -1
              ? undefined
              : locationParts[0];

          return {
            functionName,
            fileName,
            lineNumber: locationParts[1],
            columnNumber: locationParts[2],
            source: line
          };
        }, this);
      },

      parseFFOrSafari: function ErrorStackParser$$parseFFOrSafari(error) {
        let filtered = error.stack.split('\n').filter(function(line) {
          return !line.match(SAFARI_NATIVE_CODE_REGEXP);
        }, this);

        return filtered.map(function(line) {
          // Throw away eval information until we implement stacktrace.js/stackframe#8
          if (line.indexOf(' > eval') > -1) {
            line = line.replace(
              / line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g,
              ':$1'
            );
          }

          if (line.indexOf('@') === -1 && line.indexOf(':') === -1) {
            // Safari eval frames only have function names and nothing else
            return {
              functionName: line
            };
          } else {
            let functionNameRegex = /((.*".+"[^@]*)?[^@]*)(?:@)/;
            let matches = line.match(functionNameRegex);
            let functionName = matches && matches[1] ? matches[1] : undefined;
            let locationParts = this.extractLocation(
              line.replace(functionNameRegex, '')
            );

            return {
              functionName,
              fileName: locationParts[0],
              lineNumber: locationParts[1],
              columnNumber: locationParts[2],
              source: line
            };
          }
        }, this);
      },

      parseOpera: function ErrorStackParser$$parseOpera(e) {
        if (
          !e.stacktrace ||
          (e.message.indexOf('\n') > -1 &&
            e.message.split('\n').length > e.stacktrace.split('\n').length)
        ) {
          return this.parseOpera9(e);
        } else if (!e.stack) {
          return this.parseOpera10(e);
        } else {
          return this.parseOpera11(e);
        }
      },

      parseOpera9: function ErrorStackParser$$parseOpera9(e) {
        let lineRE = /Line (\d+).*script (?:in )?(\S+)/i;
        let lines = e.message.split('\n');
        let result = [];

        for (let i = 2, len = lines.length; i < len; i += 2) {
          let match = lineRE.exec(lines[i]);
          if (match) {
            result.push({
              fileName: match[2],
              lineNumber: match[1],
              source: lines[i]
            });
          }
        }

        return result;
      },

      parseOpera10: function ErrorStackParser$$parseOpera10(e) {
        let lineRE = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i;
        let lines = e.stacktrace.split('\n');
        let result = [];

        for (let i = 0, len = lines.length; i < len; i += 2) {
          let match = lineRE.exec(lines[i]);
          if (match) {
            result.push({
              functionName: match[3] || undefined,
              fileName: match[2],
              lineNumber: match[1],
              source: lines[i]
            });
          }
        }

        return result;
      },

      // Opera 10.65+ Error.stack very similar to FF/Safari
      parseOpera11: function ErrorStackParser$$parseOpera11(error) {
        let filtered = error.stack.split('\n').filter(function(line) {
          return (
            !!line.match(FIREFOX_SAFARI_STACK_REGEXP) &&
            !line.match(/^Error created at/)
          );
        }, this);

        return filtered.map(function(line) {
          let tokens = line.split('@');
          let locationParts = this.extractLocation(tokens.pop());
          let functionCall = tokens.shift() || '';
          let functionName =
            functionCall
              .replace(/<anonymous function(: (\w+))?>/, '$2')
              .replace(/\([^)]*\)/g, '') || undefined;
          let argsRaw;
          if (functionCall.match(/\(([^)]*)\)/)) {
            argsRaw = functionCall.replace(/^[^(]+\(([^)]*)\)$/, '$1');
          }
          let args =
            argsRaw === undefined || argsRaw === '[arguments not available]'
              ? undefined
              : argsRaw.split(',');

          return {
            functionName,
            args,
            fileName: locationParts[0],
            lineNumber: locationParts[1],
            columnNumber: locationParts[2],
            source: line
          };
        }, this);
      }
    };
  }

  // End borrow

  // wrapper exposing ErrorStackParser
  function stacktrace(p5, fn){
    p5._getErrorStackParser = function getErrorStackParser() {
      return new ErrorStackParser();
    };
  }

  if (typeof p5 !== 'undefined') {
    stacktrace(p5);
  }

  /** A special constant with type `never` */
  function $constructor(name, initializer, params) {
      function init(inst, def) {
          if (!inst._zod) {
              Object.defineProperty(inst, "_zod", {
                  value: {
                      def,
                      constr: _,
                      traits: new Set(),
                  },
                  enumerable: false,
              });
          }
          if (inst._zod.traits.has(name)) {
              return;
          }
          inst._zod.traits.add(name);
          initializer(inst, def);
          // support prototype modifications
          const proto = _.prototype;
          const keys = Object.keys(proto);
          for (let i = 0; i < keys.length; i++) {
              const k = keys[i];
              if (!(k in inst)) {
                  inst[k] = proto[k].bind(inst);
              }
          }
      }
      // doesn't work if Parent has a constructor with arguments
      const Parent = params?.Parent ?? Object;
      class Definition extends Parent {
      }
      Object.defineProperty(Definition, "name", { value: name });
      function _(def) {
          var _a;
          const inst = params?.Parent ? new Definition() : this;
          init(inst, def);
          (_a = inst._zod).deferred ?? (_a.deferred = []);
          for (const fn of inst._zod.deferred) {
              fn();
          }
          return inst;
      }
      Object.defineProperty(_, "init", { value: init });
      Object.defineProperty(_, Symbol.hasInstance, {
          value: (inst) => {
              if (params?.Parent && inst instanceof params.Parent)
                  return true;
              return inst?._zod?.traits?.has(name);
          },
      });
      Object.defineProperty(_, "name", { value: name });
      return _;
  }
  class $ZodAsyncError extends Error {
      constructor() {
          super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`);
      }
  }
  class $ZodEncodeError extends Error {
      constructor(name) {
          super(`Encountered unidirectional transform during encode: ${name}`);
          this.name = "ZodEncodeError";
      }
  }
  const globalConfig = {};
  function config(newConfig) {
      return globalConfig;
  }

  // functions
  function getEnumValues(entries) {
      const numericValues = Object.values(entries).filter((v) => typeof v === "number");
      const values = Object.entries(entries)
          .filter(([k, _]) => numericValues.indexOf(+k) === -1)
          .map(([_, v]) => v);
      return values;
  }
  function jsonStringifyReplacer(_, value) {
      if (typeof value === "bigint")
          return value.toString();
      return value;
  }
  function cached(getter) {
      return {
          get value() {
              {
                  const value = getter();
                  Object.defineProperty(this, "value", { value });
                  return value;
              }
          },
      };
  }
  function nullish(input) {
      return input === null || input === undefined;
  }
  function cleanRegex(source) {
      const start = source.startsWith("^") ? 1 : 0;
      const end = source.endsWith("$") ? source.length - 1 : source.length;
      return source.slice(start, end);
  }
  function floatSafeRemainder(val, step) {
      const valDecCount = (val.toString().split(".")[1] || "").length;
      const stepString = step.toString();
      let stepDecCount = (stepString.split(".")[1] || "").length;
      if (stepDecCount === 0 && /\d?e-\d?/.test(stepString)) {
          const match = stepString.match(/\d?e-(\d?)/);
          if (match?.[1]) {
              stepDecCount = Number.parseInt(match[1]);
          }
      }
      const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount;
      const valInt = Number.parseInt(val.toFixed(decCount).replace(".", ""));
      const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", ""));
      return (valInt % stepInt) / 10 ** decCount;
  }
  const EVALUATING = Symbol("evaluating");
  function defineLazy(object, key, getter) {
      let value = undefined;
      Object.defineProperty(object, key, {
          get() {
              if (value === EVALUATING) {
                  // Circular reference detected, return undefined to break the cycle
                  return undefined;
              }
              if (value === undefined) {
                  value = EVALUATING;
                  value = getter();
              }
              return value;
          },
          set(v) {
              Object.defineProperty(object, key, {
                  value: v,
                  // configurable: true,
              });
              // object[key] = v;
          },
          configurable: true,
      });
  }
  function assignProp(target, prop, value) {
      Object.defineProperty(target, prop, {
          value,
          writable: true,
          enumerable: true,
          configurable: true,
      });
  }
  function mergeDefs(...defs) {
      const mergedDescriptors = {};
      for (const def of defs) {
          const descriptors = Object.getOwnPropertyDescriptors(def);
          Object.assign(mergedDescriptors, descriptors);
      }
      return Object.defineProperties({}, mergedDescriptors);
  }
  function esc(str) {
      return JSON.stringify(str);
  }
  function slugify(input) {
      return input
          .toLowerCase()
          .trim()
          .replace(/[^\w\s-]/g, "")
          .replace(/[\s_-]+/g, "-")
          .replace(/^-+|-+$/g, "");
  }
  const captureStackTrace = ("captureStackTrace" in Error ? Error.captureStackTrace : (..._args) => { });
  function isObject(data) {
      return typeof data === "object" && data !== null && !Array.isArray(data);
  }
  const allowsEval = cached(() => {
      // @ts-ignore
      if (typeof navigator !== "undefined" && navigator?.userAgent?.includes("Cloudflare")) {
          return false;
      }
      try {
          const F = Function;
          new F("");
          return true;
      }
      catch (_) {
          return false;
      }
  });
  function isPlainObject(o) {
      if (isObject(o) === false)
          return false;
      // modified constructor
      const ctor = o.constructor;
      if (ctor === undefined)
          return true;
      if (typeof ctor !== "function")
          return true;
      // modified prototype
      const prot = ctor.prototype;
      if (isObject(prot) === false)
          return false;
      // ctor doesn't have static `isPrototypeOf`
      if (Object.prototype.hasOwnProperty.call(prot, "isPrototypeOf") === false) {
          return false;
      }
      return true;
  }
  function shallowClone(o) {
      if (isPlainObject(o))
          return { ...o };
      if (Array.isArray(o))
          return [...o];
      return o;
  }
  const propertyKeyTypes = new Set(["string", "number", "symbol"]);
  function escapeRegex(str) {
      return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  }
  // zod-specific utils
  function clone(inst, def, params) {
      const cl = new inst._zod.constr(def ?? inst._zod.def);
      if (!def || params?.parent)
          cl._zod.parent = inst;
      return cl;
  }
  function normalizeParams(_params) {
      const params = _params;
      if (!params)
          return {};
      if (typeof params === "string")
          return { error: () => params };
      if (params?.message !== undefined) {
          if (params?.error !== undefined)
              throw new Error("Cannot specify both `message` and `error` params");
          params.error = params.message;
      }
      delete params.message;
      if (typeof params.error === "string")
          return { ...params, error: () => params.error };
      return params;
  }
  function optionalKeys(shape) {
      return Object.keys(shape).filter((k) => {
          return shape[k]._zod.optin === "optional" && shape[k]._zod.optout === "optional";
      });
  }
  const NUMBER_FORMAT_RANGES = {
      safeint: [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER],
      int32: [-2147483648, 2147483647],
      uint32: [0, 4294967295],
      float32: [-34028234663852886e22, 3.4028234663852886e38],
      float64: [-Number.MAX_VALUE, Number.MAX_VALUE],
  };
  function pick(schema, mask) {
      const currDef = schema._zod.def;
      const def = mergeDefs(schema._zod.def, {
          get shape() {
              const newShape = {};
              for (const key in mask) {
                  if (!(key in currDef.shape)) {
                      throw new Error(`Unrecognized key: "${key}"`);
                  }
                  if (!mask[key])
                      continue;
                  newShape[key] = currDef.shape[key];
              }
              assignProp(this, "shape", newShape); // self-caching
              return newShape;
          },
          checks: [],
      });
      return clone(schema, def);
  }
  function omit(schema, mask) {
      const currDef = schema._zod.def;
      const def = mergeDefs(schema._zod.def, {
          get shape() {
              const newShape = { ...schema._zod.def.shape };
              for (const key in mask) {
                  if (!(key in currDef.shape)) {
                      throw new Error(`Unrecognized key: "${key}"`);
                  }
                  if (!mask[key])
                      continue;
                  delete newShape[key];
              }
              assignProp(this, "shape", newShape); // self-caching
              return newShape;
          },
          checks: [],
      });
      return clone(schema, def);
  }
  function extend(schema, shape) {
      if (!isPlainObject(shape)) {
          throw new Error("Invalid input to extend: expected a plain object");
      }
      const checks = schema._zod.def.checks;
      const hasChecks = checks && checks.length > 0;
      if (hasChecks) {
          throw new Error("Object schemas containing refinements cannot be extended. Use `.safeExtend()` instead.");
      }
      const def = mergeDefs(schema._zod.def, {
          get shape() {
              const _shape = { ...schema._zod.def.shape, ...shape };
              assignProp(this, "shape", _shape); // self-caching
              return _shape;
          },
          checks: [],
      });
      return clone(schema, def);
  }
  function safeExtend(schema, shape) {
      if (!isPlainObject(shape)) {
          throw new Error("Invalid input to safeExtend: expected a plain object");
      }
      const def = {
          ...schema._zod.def,
          get shape() {
              const _shape = { ...schema._zod.def.shape, ...shape };
              assignProp(this, "shape", _shape); // self-caching
              return _shape;
          },
          checks: schema._zod.def.checks,
      };
      return clone(schema, def);
  }
  function merge(a, b) {
      const def = mergeDefs(a._zod.def, {
          get shape() {
              const _shape = { ...a._zod.def.shape, ...b._zod.def.shape };
              assignProp(this, "shape", _shape); // self-caching
              return _shape;
          },
          get catchall() {
              return b._zod.def.catchall;
          },
          checks: [], // delete existing checks
      });
      return clone(a, def);
  }
  function partial(Class, schema, mask) {
      const def = mergeDefs(schema._zod.def, {
          get shape() {
              const oldShape = schema._zod.def.shape;
              const shape = { ...oldShape };
              if (mask) {
                  for (const key in mask) {
                      if (!(key in oldShape)) {
                          throw new Error(`Unrecognized key: "${key}"`);
                      }
                      if (!mask[key])
                          continue;
                      // if (oldShape[key]!._zod.optin === "optional") continue;
                      shape[key] = Class
                          ? new Class({
                              type: "optional",
                              innerType: oldShape[key],
                          })
                          : oldShape[key];
                  }
              }
              else {
                  for (const key in oldShape) {
                      // if (oldShape[key]!._zod.optin === "optional") continue;
                      shape[key] = Class
                          ? new Class({
                              type: "optional",
                              innerType: oldShape[key],
                          })
                          : oldShape[key];
                  }
              }
              assignProp(this, "shape", shape); // self-caching
              return shape;
          },
          checks: [],
      });
      return clone(schema, def);
  }
  function required(Class, schema, mask) {
      const def = mergeDefs(schema._zod.def, {
          get shape() {
              const oldShape = schema._zod.def.shape;
              const shape = { ...oldShape };
              if (mask) {
                  for (const key in mask) {
                      if (!(key in shape)) {
                          throw new Error(`Unrecognized key: "${key}"`);
                      }
                      if (!mask[key])
                          continue;
                      // overwrite with non-optional
                      shape[key] = new Class({
                          type: "nonoptional",
                          innerType: oldShape[key],
                      });
                  }
              }
              else {
                  for (const key in oldShape) {
                      // overwrite with non-optional
                      shape[key] = new Class({
                          type: "nonoptional",
                          innerType: oldShape[key],
                      });
                  }
              }
              assignProp(this, "shape", shape); // self-caching
              return shape;
          },
          checks: [],
      });
      return clone(schema, def);
  }
  // invalid_type | too_big | too_small | invalid_format | not_multiple_of | unrecognized_keys | invalid_union | invalid_key | invalid_element | invalid_value | custom
  function aborted(x, startIndex = 0) {
      if (x.aborted === true)
          return true;
      for (let i = startIndex; i < x.issues.length; i++) {
          if (x.issues[i]?.continue !== true) {
              return true;
          }
      }
      return false;
  }
  function prefixIssues(path, issues) {
      return issues.map((iss) => {
          var _a;
          (_a = iss).path ?? (_a.path = []);
          iss.path.unshift(path);
          return iss;
      });
  }
  function unwrapMessage(message) {
      return typeof message === "string" ? message : message?.message;
  }
  function finalizeIssue(iss, ctx, config) {
      const full = { ...iss, path: iss.path ?? [] };
      // for backwards compatibility
      if (!iss.message) {
          const message = unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ??
              unwrapMessage(ctx?.error?.(iss)) ??
              unwrapMessage(config.customError?.(iss)) ??
              unwrapMessage(config.localeError?.(iss)) ??
              "Invalid input";
          full.message = message;
      }
      // delete (full as any).def;
      delete full.inst;
      delete full.continue;
      if (!ctx?.reportInput) {
          delete full.input;
      }
      return full;
  }
  function getLengthableOrigin(input) {
      if (Array.isArray(input))
          return "array";
      if (typeof input === "string")
          return "string";
      return "unknown";
  }
  function issue(...args) {
      const [iss, input, inst] = args;
      if (typeof iss === "string") {
          return {
              message: iss,
              code: "custom",
              input,
              inst,
          };
      }
      return { ...iss };
  }

  const initializer$1 = (inst, def) => {
      inst.name = "$ZodError";
      Object.defineProperty(inst, "_zod", {
          value: inst._zod,
          enumerable: false,
      });
      Object.defineProperty(inst, "issues", {
          value: def,
          enumerable: false,
      });
      inst.message = JSON.stringify(def, jsonStringifyReplacer, 2);
      Object.defineProperty(inst, "toString", {
          value: () => inst.message,
          enumerable: false,
      });
  };
  const $ZodError = $constructor("$ZodError", initializer$1);
  const $ZodRealError = $constructor("$ZodError", initializer$1, { Parent: Error });
  function flattenError(error, mapper = (issue) => issue.message) {
      const fieldErrors = {};
      const formErrors = [];
      for (const sub of error.issues) {
          if (sub.path.length > 0) {
              fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || [];
              fieldErrors[sub.path[0]].push(mapper(sub));
          }
          else {
              formErrors.push(mapper(sub));
          }
      }
      return { formErrors, fieldErrors };
  }
  function formatError(error, mapper = (issue) => issue.message) {
      const fieldErrors = { _errors: [] };
      const processError = (error) => {
          for (const issue of error.issues) {
              if (issue.code === "invalid_union" && issue.errors.length) {
                  issue.errors.map((issues) => processError({ issues }));
              }
              else if (issue.code === "invalid_key") {
                  processError({ issues: issue.issues });
              }
              else if (issue.code === "invalid_element") {
                  processError({ issues: issue.issues });
              }
              else if (issue.path.length === 0) {
                  fieldErrors._errors.push(mapper(issue));
              }
              else {
                  let curr = fieldErrors;
                  let i = 0;
                  while (i < issue.path.length) {
                      const el = issue.path[i];
                      const terminal = i === issue.path.length - 1;
                      if (!terminal) {
                          curr[el] = curr[el] || { _errors: [] };
                      }
                      else {
                          curr[el] = curr[el] || { _errors: [] };
                          curr[el]._errors.push(mapper(issue));
                      }
                      curr = curr[el];
                      i++;
                  }
              }
          }
      };
      processError(error);
      return fieldErrors;
  }

  const _parse = (_Err) => (schema, value, _ctx, _params) => {
      const ctx = _ctx ? Object.assign(_ctx, { async: false }) : { async: false };
      const result = schema._zod.run({ value, issues: [] }, ctx);
      if (result instanceof Promise) {
          throw new $ZodAsyncError();
      }
      if (result.issues.length) {
          const e = new (_params?.Err ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));
          captureStackTrace(e, _params?.callee);
          throw e;
      }
      return result.value;
  };
  const parse$2 = /* @__PURE__*/ _parse($ZodRealError);
  const _parseAsync = (_Err) => async (schema, value, _ctx, params) => {
      const ctx = _ctx ? Object.assign(_ctx, { async: true }) : { async: true };
      let result = schema._zod.run({ value, issues: [] }, ctx);
      if (result instanceof Promise)
          result = await result;
      if (result.issues.length) {
          const e = new (params?.Err ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));
          captureStackTrace(e, params?.callee);
          throw e;
      }
      return result.value;
  };
  const parseAsync$1 = /* @__PURE__*/ _parseAsync($ZodRealError);
  const _safeParse = (_Err) => (schema, value, _ctx) => {
      const ctx = _ctx ? { ..._ctx, async: false } : { async: false };
      const result = schema._zod.run({ value, issues: [] }, ctx);
      if (result instanceof Promise) {
          throw new $ZodAsyncError();
      }
      return result.issues.length
          ? {
              success: false,
              error: new (_Err ?? $ZodError)(result.issues.map((iss) => finalizeIssue(iss, ctx, config()))),
          }
          : { success: true, data: result.value };
  };
  const safeParse$1 = /* @__PURE__*/ _safeParse($ZodRealError);
  const _safeParseAsync = (_Err) => async (schema, value, _ctx) => {
      const ctx = _ctx ? Object.assign(_ctx, { async: true }) : { async: true };
      let result = schema._zod.run({ value, issues: [] }, ctx);
      if (result instanceof Promise)
          result = await result;
      return result.issues.length
          ? {
              success: false,
              error: new _Err(result.issues.map((iss) => finalizeIssue(iss, ctx, config()))),
          }
          : { success: true, data: result.value };
  };
  const safeParseAsync$1 = /* @__PURE__*/ _safeParseAsync($ZodRealError);
  const _encode = (_Err) => (schema, value, _ctx) => {
      const ctx = _ctx ? Object.assign(_ctx, { direction: "backward" }) : { direction: "backward" };
      return _parse(_Err)(schema, value, ctx);
  };
  const _decode = (_Err) => (schema, value, _ctx) => {
      return _parse(_Err)(schema, value, _ctx);
  };
  const _encodeAsync = (_Err) => async (schema, value, _ctx) => {
      const ctx = _ctx ? Object.assign(_ctx, { direction: "backward" }) : { direction: "backward" };
      return _parseAsync(_Err)(schema, value, ctx);
  };
  const _decodeAsync = (_Err) => async (schema, value, _ctx) => {
      return _parseAsync(_Err)(schema, value, _ctx);
  };
  const _safeEncode = (_Err) => (schema, value, _ctx) => {
      const ctx = _ctx ? Object.assign(_ctx, { direction: "backward" }) : { direction: "backward" };
      return _safeParse(_Err)(schema, value, ctx);
  };
  const _safeDecode = (_Err) => (schema, value, _ctx) => {
      return _safeParse(_Err)(schema, value, _ctx);
  };
  const _safeEncodeAsync = (_Err) => async (schema, value, _ctx) => {
      const ctx = _ctx ? Object.assign(_ctx, { direction: "backward" }) : { direction: "backward" };
      return _safeParseAsync(_Err)(schema, value, ctx);
  };
  const _safeDecodeAsync = (_Err) => async (schema, value, _ctx) => {
      return _safeParseAsync(_Err)(schema, value, _ctx);
  };

  const cuid = /^[cC][^\s-]{8,}$/;
  const cuid2 = /^[0-9a-z]+$/;
  const ulid = /^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/;
  const xid = /^[0-9a-vA-V]{20}$/;
  const ksuid = /^[A-Za-z0-9]{27}$/;
  const nanoid = /^[a-zA-Z0-9_-]{21}$/;
  /** ISO 8601-1 duration regex. Does not support the 8601-2 extensions like negative durations or fractional/negative components. */
  const duration$1 = /^P(?:(\d+W)|(?!.*W)(?=\d|T\d)(\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+([.,]\d+)?S)?)?)$/;
  /** A regex for any UUID-like identifier: 8-4-4-4-12 hex pattern */
  const guid = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;
  /** Returns a regex for validating an RFC 9562/4122 UUID.
   *
   * @param version Optionally specify a version 1-8. If no version is specified, all versions are supported. */
  const uuid = (version) => {
      if (!version)
          return /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/;
      return new RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${version}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`);
  };
  /** Practical email validation */
  const email = /^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$/;
  // from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression
  const _emoji$1 = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`;
  function emoji() {
      return new RegExp(_emoji$1, "u");
  }
  const ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
  const ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
  const cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/;
  const cidrv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/;
  // https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript
  const base64$1 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/;
  const base64url = /^[A-Za-z0-9_-]*$/;
  // https://blog.stevenlevithan.com/archives/validate-phone-number#r4-3 (regex sans spaces)
  const e164 = /^\+(?:[0-9]){6,14}[0-9]$/;
  // const dateSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`;
  const dateSource = `(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))`;
  const date$1 = /*@__PURE__*/ new RegExp(`^${dateSource}$`);
  function timeSource(args) {
      const hhmm = `(?:[01]\\d|2[0-3]):[0-5]\\d`;
      const regex = typeof args.precision === "number"
          ? args.precision === -1
              ? `${hhmm}`
              : args.precision === 0
                  ? `${hhmm}:[0-5]\\d`
                  : `${hhmm}:[0-5]\\d\\.\\d{${args.precision}}`
          : `${hhmm}(?::[0-5]\\d(?:\\.\\d+)?)?`;
      return regex;
  }
  function time$1(args) {
      return new RegExp(`^${timeSource(args)}$`);
  }
  // Adapted from https://stackoverflow.com/a/3143231
  function datetime$1(args) {
      const time = timeSource({ precision: args.precision });
      const opts = ["Z"];
      if (args.local)
          opts.push("");
      // if (args.offset) opts.push(`([+-]\\d{2}:\\d{2})`);
      if (args.offset)
          opts.push(`([+-](?:[01]\\d|2[0-3]):[0-5]\\d)`);
      const timeRegex = `${time}(?:${opts.join("|")})`;
      return new RegExp(`^${dateSource}T(?:${timeRegex})$`);
  }
  const string$1 = (params) => {
      const regex = params ? `[\\s\\S]{${params?.minimum ?? 0},${params?.maximum ?? ""}}` : `[\\s\\S]*`;
      return new RegExp(`^${regex}$`);
  };
  const integer = /^-?\d+$/;
  const number$1 = /^-?\d+(?:\.\d+)?/;
  const boolean$1 = /^(?:true|false)$/i;
  // regex for string with no uppercase letters
  const lowercase = /^[^A-Z]*$/;
  // regex for string with no lowercase letters
  const uppercase = /^[^a-z]*$/;

  // import { $ZodType } from "./schemas.js";
  const $ZodCheck = /*@__PURE__*/ $constructor("$ZodCheck", (inst, def) => {
      var _a;
      inst._zod ?? (inst._zod = {});
      inst._zod.def = def;
      (_a = inst._zod).onattach ?? (_a.onattach = []);
  });
  const numericOriginMap = {
      number: "number",
      bigint: "bigint",
      object: "date",
  };
  const $ZodCheckLessThan = /*@__PURE__*/ $constructor("$ZodCheckLessThan", (inst, def) => {
      $ZodCheck.init(inst, def);
      const origin = numericOriginMap[typeof def.value];
      inst._zod.onattach.push((inst) => {
          const bag = inst._zod.bag;
          const curr = (def.inclusive ? bag.maximum : bag.exclusiveMaximum) ?? Number.POSITIVE_INFINITY;
          if (def.value < curr) {
              if (def.inclusive)
                  bag.maximum = def.value;
              else
                  bag.exclusiveMaximum = def.value;
          }
      });
      inst._zod.check = (payload) => {
          if (def.inclusive ? payload.value <= def.value : payload.value < def.value) {
              return;
          }
          payload.issues.push({
              origin,
              code: "too_big",
              maximum: def.value,
              input: payload.value,
              inclusive: def.inclusive,
              inst,
              continue: !def.abort,
          });
      };
  });
  const $ZodCheckGreaterThan = /*@__PURE__*/ $constructor("$ZodCheckGreaterThan", (inst, def) => {
      $ZodCheck.init(inst, def);
      const origin = numericOriginMap[typeof def.value];
      inst._zod.onattach.push((inst) => {
          const bag = inst._zod.bag;
          const curr = (def.inclusive ? bag.minimum : bag.exclusiveMinimum) ?? Number.NEGATIVE_INFINITY;
          if (def.value > curr) {
              if (def.inclusive)
                  bag.minimum = def.value;
              else
                  bag.exclusiveMinimum = def.value;
          }
      });
      inst._zod.check = (payload) => {
          if (def.inclusive ? payload.value >= def.value : payload.value > def.value) {
              return;
          }
          payload.issues.push({
              origin,
              code: "too_small",
              minimum: def.value,
              input: payload.value,
              inclusive: def.inclusive,
              inst,
              continue: !def.abort,
          });
      };
  });
  const $ZodCheckMultipleOf = 
  /*@__PURE__*/ $constructor("$ZodCheckMultipleOf", (inst, def) => {
      $ZodCheck.init(inst, def);
      inst._zod.onattach.push((inst) => {
          var _a;
          (_a = inst._zod.bag).multipleOf ?? (_a.multipleOf = def.value);
      });
      inst._zod.check = (payload) => {
          if (typeof payload.value !== typeof def.value)
              throw new Error("Cannot mix number and bigint in multiple_of check.");
          const isMultiple = typeof payload.value === "bigint"
              ? payload.value % def.value === BigInt(0)
              : floatSafeRemainder(payload.value, def.value) === 0;
          if (isMultiple)
              return;
          payload.issues.push({
              origin: typeof payload.value,
              code: "not_multiple_of",
              divisor: def.value,
              input: payload.value,
              inst,
              continue: !def.abort,
          });
      };
  });
  const $ZodCheckNumberFormat = /*@__PURE__*/ $constructor("$ZodCheckNumberFormat", (inst, def) => {
      $ZodCheck.init(inst, def); // no format checks
      def.format = def.format || "float64";
      const isInt = def.format?.includes("int");
      const origin = isInt ? "int" : "number";
      const [minimum, maximum] = NUMBER_FORMAT_RANGES[def.format];
      inst._zod.onattach.push((inst) => {
          const bag = inst._zod.bag;
          bag.format = def.format;
          bag.minimum = minimum;
          bag.maximum = maximum;
          if (isInt)
              bag.pattern = integer;
      });
      inst._zod.check = (payload) => {
          const input = payload.value;
          if (isInt) {
              if (!Number.isInteger(input)) {
                  // invalid_format issue
                  // payload.issues.push({
                  //   expected: def.format,
                  //   format: def.format,
                  //   code: "invalid_format",
                  //   input,
                  //   inst,
                  // });
                  // invalid_type issue
                  payload.issues.push({
                      expected: origin,
                      format: def.format,
                      code: "invalid_type",
                      continue: false,
                      input,
                      inst,
                  });
                  return;
                  // not_multiple_of issue
                  // payload.issues.push({
                  //   code: "not_multiple_of",
                  //   origin: "number",
                  //   input,
                  //   inst,
                  //   divisor: 1,
                  // });
              }
              if (!Number.isSafeInteger(input)) {
                  if (input > 0) {
                      // too_big
                      payload.issues.push({
                          input,
                          code: "too_big",
                          maximum: Number.MAX_SAFE_INTEGER,
                          note: "Integers must be within the safe integer range.",
                          inst,
                          origin,
                          continue: !def.abort,
                      });
                  }
                  else {
                      // too_small
                      payload.issues.push({
                          input,
                          code: "too_small",
                          minimum: Number.MIN_SAFE_INTEGER,
                          note: "Integers must be within the safe integer range.",
                          inst,
                          origin,
                          continue: !def.abort,
                      });
                  }
                  return;
              }
          }
          if (input < minimum) {
              payload.issues.push({
                  origin: "number",
                  input,
                  code: "too_small",
                  minimum,
                  inclusive: true,
                  inst,
                  continue: !def.abort,
              });
          }
          if (input > maximum) {
              payload.issues.push({
                  origin: "number",
                  input,
                  code: "too_big",
                  maximum,
                  inst,
              });
          }
      };
  });
  const $ZodCheckMaxLength = /*@__PURE__*/ $constructor("$ZodCheckMaxLength", (inst, def) => {
      var _a;
      $ZodCheck.init(inst, def);
      (_a = inst._zod.def).when ?? (_a.when = (payload) => {
          const val = payload.value;
          return !nullish(val) && val.length !== undefined;
      });
      inst._zod.onattach.push((inst) => {
          const curr = (inst._zod.bag.maximum ?? Number.POSITIVE_INFINITY);
          if (def.maximum < curr)
              inst._zod.bag.maximum = def.maximum;
      });
      inst._zod.check = (payload) => {
          const input = payload.value;
          const length = input.length;
          if (length <= def.maximum)
              return;
          const origin = getLengthableOrigin(input);
          payload.issues.push({
              origin,
              code: "too_big",
              maximum: def.maximum,
              inclusive: true,
              input,
              inst,
              continue: !def.abort,
          });
      };
  });
  const $ZodCheckMinLength = /*@__PURE__*/ $constructor("$ZodCheckMinLength", (inst, def) => {
      var _a;
      $ZodCheck.init(inst, def);
      (_a = inst._zod.def).when ?? (_a.when = (payload) => {
          const val = payload.value;
          return !nullish(val) && val.length !== undefined;
      });
      inst._zod.onattach.push((inst) => {
          const curr = (inst._zod.bag.minimum ?? Number.NEGATIVE_INFINITY);
          if (def.minimum > curr)
              inst._zod.bag.minimum = def.minimum;
      });
      inst._zod.check = (payload) => {
          const input = payload.value;
          const length = input.length;
          if (length >= def.minimum)
              return;
          const origin = getLengthableOrigin(input);
          payload.issues.push({
              origin,
              code: "too_small",
              minimum: def.minimum,
              inclusive: true,
              input,
              inst,
              continue: !def.abort,
          });
      };
  });
  const $ZodCheckLengthEquals = /*@__PURE__*/ $constructor("$ZodCheckLengthEquals", (inst, def) => {
      var _a;
      $ZodCheck.init(inst, def);
      (_a = inst._zod.def).when ?? (_a.when = (payload) => {
          const val = payload.value;
          return !nullish(val) && val.length !== undefined;
      });
      inst._zod.onattach.push((inst) => {
          const bag = inst._zod.bag;
          bag.minimum = def.length;
          bag.maximum = def.length;
          bag.length = def.length;
      });
      inst._zod.check = (payload) => {
          const input = payload.value;
          const length = input.length;
          if (length === def.length)
              return;
          const origin = getLengthableOrigin(input);
          const tooBig = length > def.length;
          payload.issues.push({
              origin,
              ...(tooBig ? { code: "too_big", maximum: def.length } : { code: "too_small", minimum: def.length }),
              inclusive: true,
              exact: true,
              input: payload.value,
              inst,
              continue: !def.abort,
          });
      };
  });
  const $ZodCheckStringFormat = /*@__PURE__*/ $constructor("$ZodCheckStringFormat", (inst, def) => {
      var _a, _b;
      $ZodCheck.init(inst, def);
      inst._zod.onattach.push((inst) => {
          const bag = inst._zod.bag;
          bag.format = def.format;
          if (def.pattern) {
              bag.patterns ?? (bag.patterns = new Set());
              bag.patterns.add(def.pattern);
          }
      });
      if (def.pattern)
          (_a = inst._zod).check ?? (_a.check = (payload) => {
              def.pattern.lastIndex = 0;
              if (def.pattern.test(payload.value))
                  return;
              payload.issues.push({
                  origin: "string",
                  code: "invalid_format",
                  format: def.format,
                  input: payload.value,
                  ...(def.pattern ? { pattern: def.pattern.toString() } : {}),
                  inst,
                  continue: !def.abort,
              });
          });
      else
          (_b = inst._zod).check ?? (_b.check = () => { });
  });
  const $ZodCheckRegex = /*@__PURE__*/ $constructor("$ZodCheckRegex", (inst, def) => {
      $ZodCheckStringFormat.init(inst, def);
      inst._zod.check = (payload) => {
          def.pattern.lastIndex = 0;
          if (def.pattern.test(payload.value))
              return;
          payload.issues.push({
              origin: "string",
              code: "invalid_format",
              format: "regex",
              input: payload.value,
              pattern: def.pattern.toString(),
              inst,
              continue: !def.abort,
          });
      };
  });
  const $ZodCheckLowerCase = /*@__PURE__*/ $constructor("$ZodCheckLowerCase", (inst, def) => {
      def.pattern ?? (def.pattern = lowercase);
      $ZodCheckStringFormat.init(inst, def);
  });
  const $ZodCheckUpperCase = /*@__PURE__*/ $constructor("$ZodCheckUpperCase", (inst, def) => {
      def.pattern ?? (def.pattern = uppercase);
      $ZodCheckStringFormat.init(inst, def);
  });
  const $ZodCheckIncludes = /*@__PURE__*/ $constructor("$ZodCheckIncludes", (inst, def) => {
      $ZodCheck.init(inst, def);
      const escapedRegex = escapeRegex(def.includes);
      const pattern = new RegExp(typeof def.position === "number" ? `^.{${def.position}}${escapedRegex}` : escapedRegex);
      def.pattern = pattern;
      inst._zod.onattach.push((inst) => {
          const bag = inst._zod.bag;
          bag.patterns ?? (bag.patterns = new Set());
          bag.patterns.add(pattern);
      });
      inst._zod.check = (payload) => {
          if (payload.value.includes(def.includes, def.position))
              return;
          payload.issues.push({
              origin: "string",
              code: "invalid_format",
              format: "includes",
              includes: def.includes,
              input: payload.value,
              inst,
              continue: !def.abort,
          });
      };
  });
  const $ZodCheckStartsWith = /*@__PURE__*/ $constructor("$ZodCheckStartsWith", (inst, def) => {
      $ZodCheck.init(inst, def);
      const pattern = new RegExp(`^${escapeRegex(def.prefix)}.*`);
      def.pattern ?? (def.pattern = pattern);
      inst._zod.onattach.push((inst) => {
          const bag = inst._zod.bag;
          bag.patterns ?? (bag.patterns = new Set());
          bag.patterns.add(pattern);
      });
      inst._zod.check = (payload) => {
          if (payload.value.startsWith(def.prefix))
              return;
          payload.issues.push({
              origin: "string",
              code: "invalid_format",
              format: "starts_with",
              prefix: def.prefix,
              input: payload.value,
              inst,
              continue: !def.abort,
          });
      };
  });
  const $ZodCheckEndsWith = /*@__PURE__*/ $constructor("$ZodCheckEndsWith", (inst, def) => {
      $ZodCheck.init(inst, def);
      const pattern = new RegExp(`.*${escapeRegex(def.suffix)}$`);
      def.pattern ?? (def.pattern = pattern);
      inst._zod.onattach.push((inst) => {
          const bag = inst._zod.bag;
          bag.patterns ?? (bag.patterns = new Set());
          bag.patterns.add(pattern);
      });
      inst._zod.check = (payload) => {
          if (payload.value.endsWith(def.suffix))
              return;
          payload.issues.push({
              origin: "string",
              code: "invalid_format",
              format: "ends_with",
              suffix: def.suffix,
              input: payload.value,
              inst,
              continue: !def.abort,
          });
      };
  });
  const $ZodCheckOverwrite = /*@__PURE__*/ $constructor("$ZodCheckOverwrite", (inst, def) => {
      $ZodCheck.init(inst, def);
      inst._zod.check = (payload) => {
          payload.value = def.tx(payload.value);
      };
  });

  class Doc {
      constructor(args = []) {
          this.content = [];
          this.indent = 0;
          if (this)
              this.args = args;
      }
      indented(fn) {
          this.indent += 1;
          fn(this);
          this.indent -= 1;
      }
      write(arg) {
          if (typeof arg === "function") {
              arg(this, { execution: "sync" });
              arg(this, { execution: "async" });
              return;
          }
          const content = arg;
          const lines = content.split("\n").filter((x) => x);
          const minIndent = Math.min(...lines.map((x) => x.length - x.trimStart().length));
          const dedented = lines.map((x) => x.slice(minIndent)).map((x) => " ".repeat(this.indent * 2) + x);
          for (const line of dedented) {
              this.content.push(line);
          }
      }
      compile() {
          const F = Function;
          const args = this?.args;
          const content = this?.content ?? [``];
          const lines = [...content.map((x) => `  ${x}`)];
          // console.log(lines.join("\n"));
          return new F(...args, lines.join("\n"));
      }
  }

  const version$2 = {
      major: 4,
      minor: 2,
      patch: 1,
  };

  const $ZodType = /*@__PURE__*/ $constructor("$ZodType", (inst, def) => {
      var _a;
      inst ?? (inst = {});
      inst._zod.def = def; // set _def property
      inst._zod.bag = inst._zod.bag || {}; // initialize _bag object
      inst._zod.version = version$2;
      const checks = [...(inst._zod.def.checks ?? [])];
      // if inst is itself a checks.$ZodCheck, run it as a check
      if (inst._zod.traits.has("$ZodCheck")) {
          checks.unshift(inst);
      }
      for (const ch of checks) {
          for (const fn of ch._zod.onattach) {
              fn(inst);
          }
      }
      if (checks.length === 0) {
          // deferred initializer
          // inst._zod.parse is not yet defined
          (_a = inst._zod).deferred ?? (_a.deferred = []);
          inst._zod.deferred?.push(() => {
              inst._zod.run = inst._zod.parse;
          });
      }
      else {
          const runChecks = (payload, checks, ctx) => {
              let isAborted = aborted(payload);
              let asyncResult;
              for (const ch of checks) {
                  if (ch._zod.def.when) {
                      const shouldRun = ch._zod.def.when(payload);
                      if (!shouldRun)
                          continue;
                  }
                  else if (isAborted) {
                      continue;
                  }
                  const currLen = payload.issues.length;
                  const _ = ch._zod.check(payload);
                  if (_ instanceof Promise && ctx?.async === false) {
                      throw new $ZodAsyncError();
                  }
                  if (asyncResult || _ instanceof Promise) {
                      asyncResult = (asyncResult ?? Promise.resolve()).then(async () => {
                          await _;
                          const nextLen = payload.issues.length;
                          if (nextLen === currLen)
                              return;
                          if (!isAborted)
                              isAborted = aborted(payload, currLen);
                      });
                  }
                  else {
                      const nextLen = payload.issues.length;
                      if (nextLen === currLen)
                          continue;
                      if (!isAborted)
                          isAborted = aborted(payload, currLen);
                  }
              }
              if (asyncResult) {
                  return asyncResult.then(() => {
                      return payload;
                  });
              }
              return payload;
          };
          const handleCanaryResult = (canary, payload, ctx) => {
              // abort if the canary is aborted
              if (aborted(canary)) {
                  canary.aborted = true;
                  return canary;
              }
              // run checks first, then
              const checkResult = runChecks(payload, checks, ctx);
              if (checkResult instanceof Promise) {
                  if (ctx.async === false)
                      throw new $ZodAsyncError();
                  return checkResult.then((checkResult) => inst._zod.parse(checkResult, ctx));
              }
              return inst._zod.parse(checkResult, ctx);
          };
          inst._zod.run = (payload, ctx) => {
              if (ctx.skipChecks) {
                  return inst._zod.parse(payload, ctx);
              }
              if (ctx.direction === "backward") {
                  // run canary
                  // initial pass (no checks)
                  const canary = inst._zod.parse({ value: payload.value, issues: [] }, { ...ctx, skipChecks: true });
                  if (canary instanceof Promise) {
                      return canary.then((canary) => {
                          return handleCanaryResult(canary, payload, ctx);
                      });
                  }
                  return handleCanaryResult(canary, payload, ctx);
              }
              // forward
              const result = inst._zod.parse(payload, ctx);
              if (result instanceof Promise) {
                  if (ctx.async === false)
                      throw new $ZodAsyncError();
                  return result.then((result) => runChecks(result, checks, ctx));
              }
              return runChecks(result, checks, ctx);
          };
      }
      inst["~standard"] = {
          validate: (value) => {
              try {
                  const r = safeParse$1(inst, value);
                  return r.success ? { value: r.data } : { issues: r.error?.issues };
              }
              catch (_) {
                  return safeParseAsync$1(inst, value).then((r) => (r.success ? { value: r.data } : { issues: r.error?.issues }));
              }
          },
          vendor: "zod",
          version: 1,
      };
  });
  const $ZodString = /*@__PURE__*/ $constructor("$ZodString", (inst, def) => {
      $ZodType.init(inst, def);
      inst._zod.pattern = [...(inst?._zod.bag?.patterns ?? [])].pop() ?? string$1(inst._zod.bag);
      inst._zod.parse = (payload, _) => {
          if (def.coerce)
              try {
                  payload.value = String(payload.value);
              }
              catch (_) { }
          if (typeof payload.value === "string")
              return payload;
          payload.issues.push({
              expected: "string",
              code: "invalid_type",
              input: payload.value,
              inst,
          });
          return payload;
      };
  });
  const $ZodStringFormat = /*@__PURE__*/ $constructor("$ZodStringFormat", (inst, def) => {
      // check initialization must come first
      $ZodCheckStringFormat.init(inst, def);
      $ZodString.init(inst, def);
  });
  const $ZodGUID = /*@__PURE__*/ $constructor("$ZodGUID", (inst, def) => {
      def.pattern ?? (def.pattern = guid);
      $ZodStringFormat.init(inst, def);
  });
  const $ZodUUID = /*@__PURE__*/ $constructor("$ZodUUID", (inst, def) => {
      if (def.version) {
          const versionMap = {
              v1: 1,
              v2: 2,
              v3: 3,
              v4: 4,
              v5: 5,
              v6: 6,
              v7: 7,
              v8: 8,
          };
          const v = versionMap[def.version];
          if (v === undefined)
              throw new Error(`Invalid UUID version: "${def.version}"`);
          def.pattern ?? (def.pattern = uuid(v));
      }
      else
          def.pattern ?? (def.pattern = uuid());
      $ZodStringFormat.init(inst, def);
  });
  const $ZodEmail = /*@__PURE__*/ $constructor("$ZodEmail", (inst, def) => {
      def.pattern ?? (def.pattern = email);
      $ZodStringFormat.init(inst, def);
  });
  const $ZodURL = /*@__PURE__*/ $constructor("$ZodURL", (inst, def) => {
      $ZodStringFormat.init(inst, def);
      inst._zod.check = (payload) => {
          try {
              // Trim whitespace from input
              const trimmed = payload.value.trim();
              // @ts-ignore
              const url = new URL(trimmed);
              if (def.hostname) {
                  def.hostname.lastIndex = 0;
                  if (!def.hostname.test(url.hostname)) {
                      payload.issues.push({
                          code: "invalid_format",
                          format: "url",
                          note: "Invalid hostname",
                          pattern: def.hostname.source,
                          input: payload.value,
                          inst,
                          continue: !def.abort,
                      });
                  }
              }
              if (def.protocol) {
                  def.protocol.lastIndex = 0;
                  if (!def.protocol.test(url.protocol.endsWith(":") ? url.protocol.slice(0, -1) : url.protocol)) {
                      payload.issues.push({
                          code: "invalid_format",
                          format: "url",
                          note: "Invalid protocol",
                          pattern: def.protocol.source,
                          input: payload.value,
                          inst,
                          continue: !def.abort,
                      });
                  }
              }
              // Set the output value based on normalize flag
              if (def.normalize) {
                  // Use normalized URL
                  payload.value = url.href;
              }
              else {
                  // Preserve the original input (trimmed)
                  payload.value = trimmed;
              }
              return;
          }
          catch (_) {
              payload.issues.push({
                  code: "invalid_format",
                  format: "url",
                  input: payload.value,
                  inst,
                  continue: !def.abort,
              });
          }
      };
  });
  const $ZodEmoji = /*@__PURE__*/ $constructor("$ZodEmoji", (inst, def) => {
      def.pattern ?? (def.pattern = emoji());
      $ZodStringFormat.init(inst, def);
  });
  const $ZodNanoID = /*@__PURE__*/ $constructor("$ZodNanoID", (inst, def) => {
      def.pattern ?? (def.pattern = nanoid);
      $ZodStringFormat.init(inst, def);
  });
  const $ZodCUID = /*@__PURE__*/ $constructor("$ZodCUID", (inst, def) => {
      def.pattern ?? (def.pattern = cuid);
      $ZodStringFormat.init(inst, def);
  });
  const $ZodCUID2 = /*@__PURE__*/ $constructor("$ZodCUID2", (inst, def) => {
      def.pattern ?? (def.pattern = cuid2);
      $ZodStringFormat.init(inst, def);
  });
  const $ZodULID = /*@__PURE__*/ $constructor("$ZodULID", (inst, def) => {
      def.pattern ?? (def.pattern = ulid);
      $ZodStringFormat.init(inst, def);
  });
  const $ZodXID = /*@__PURE__*/ $constructor("$ZodXID", (inst, def) => {
      def.pattern ?? (def.pattern = xid);
      $ZodStringFormat.init(inst, def);
  });
  const $ZodKSUID = /*@__PURE__*/ $constructor("$ZodKSUID", (inst, def) => {
      def.pattern ?? (def.pattern = ksuid);
      $ZodStringFormat.init(inst, def);
  });
  const $ZodISODateTime = /*@__PURE__*/ $constructor("$ZodISODateTime", (inst, def) => {
      def.pattern ?? (def.pattern = datetime$1(def));
      $ZodStringFormat.init(inst, def);
  });
  const $ZodISODate = /*@__PURE__*/ $constructor("$ZodISODate", (inst, def) => {
      def.pattern ?? (def.pattern = date$1);
      $ZodStringFormat.init(inst, def);
  });
  const $ZodISOTime = /*@__PURE__*/ $constructor("$ZodISOTime", (inst, def) => {
      def.pattern ?? (def.pattern = time$1(def));
      $ZodStringFormat.init(inst, def);
  });
  const $ZodISODuration = /*@__PURE__*/ $constructor("$ZodISODuration", (inst, def) => {
      def.pattern ?? (def.pattern = duration$1);
      $ZodStringFormat.init(inst, def);
  });
  const $ZodIPv4 = /*@__PURE__*/ $constructor("$ZodIPv4", (inst, def) => {
      def.pattern ?? (def.pattern = ipv4);
      $ZodStringFormat.init(inst, def);
      inst._zod.bag.format = `ipv4`;
  });
  const $ZodIPv6 = /*@__PURE__*/ $constructor("$ZodIPv6", (inst, def) => {
      def.pattern ?? (def.pattern = ipv6);
      $ZodStringFormat.init(inst, def);
      inst._zod.bag.format = `ipv6`;
      inst._zod.check = (payload) => {
          try {
              // @ts-ignore
              new URL(`http://[${payload.value}]`);
              // return;
          }
          catch {
              payload.issues.push({
                  code: "invalid_format",
                  format: "ipv6",
                  input: payload.value,
                  inst,
                  continue: !def.abort,
              });
          }
      };
  });
  const $ZodCIDRv4 = /*@__PURE__*/ $constructor("$ZodCIDRv4", (inst, def) => {
      def.pattern ?? (def.pattern = cidrv4);
      $ZodStringFormat.init(inst, def);
  });
  const $ZodCIDRv6 = /*@__PURE__*/ $constructor("$ZodCIDRv6", (inst, def) => {
      def.pattern ?? (def.pattern = cidrv6); // not used for validation
      $ZodStringFormat.init(inst, def);
      inst._zod.check = (payload) => {
          const parts = payload.value.split("/");
          try {
              if (parts.length !== 2)
                  throw new Error();
              const [address, prefix] = parts;
              if (!prefix)
                  throw new Error();
              const prefixNum = Number(prefix);
              if (`${prefixNum}` !== prefix)
                  throw new Error();
              if (prefixNum < 0 || prefixNum > 128)
                  throw new Error();
              // @ts-ignore
              new URL(`http://[${address}]`);
          }
          catch {
              payload.issues.push({
                  code: "invalid_format",
                  format: "cidrv6",
                  input: payload.value,
                  inst,
                  continue: !def.abort,
              });
          }
      };
  });
  //////////////////////////////   ZodBase64   //////////////////////////////
  function isValidBase64(data) {
      if (data === "")
          return true;
      if (data.length % 4 !== 0)
          return false;
      try {
          // @ts-ignore
          atob(data);
          return true;
      }
      catch {
          return false;
      }
  }
  const $ZodBase64 = /*@__PURE__*/ $constructor("$ZodBase64", (inst, def) => {
      def.pattern ?? (def.pattern = base64$1);
      $ZodStringFormat.init(inst, def);
      inst._zod.bag.contentEncoding = "base64";
      inst._zod.check = (payload) => {
          if (isValidBase64(payload.value))
              return;
          payload.issues.push({
              code: "invalid_format",
              format: "base64",
              input: payload.value,
              inst,
              continue: !def.abort,
          });
      };
  });
  //////////////////////////////   ZodBase64   //////////////////////////////
  function isValidBase64URL(data) {
      if (!base64url.test(data))
          return false;
      const base64 = data.replace(/[-_]/g, (c) => (c === "-" ? "+" : "/"));
      const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
      return isValidBase64(padded);
  }
  const $ZodBase64URL = /*@__PURE__*/ $constructor("$ZodBase64URL", (inst, def) => {
      def.pattern ?? (def.pattern = base64url);
      $ZodStringFormat.init(inst, def);
      inst._zod.bag.contentEncoding = "base64url";
      inst._zod.check = (payload) => {
          if (isValidBase64URL(payload.value))
              return;
          payload.issues.push({
              code: "invalid_format",
              format: "base64url",
              input: payload.value,
              inst,
              continue: !def.abort,
          });
      };
  });
  const $ZodE164 = /*@__PURE__*/ $constructor("$ZodE164", (inst, def) => {
      def.pattern ?? (def.pattern = e164);
      $ZodStringFormat.init(inst, def);
  });
  //////////////////////////////   ZodJWT   //////////////////////////////
  function isValidJWT(token, algorithm = null) {
      try {
          const tokensParts = token.split(".");
          if (tokensParts.length !== 3)
              return false;
          const [header] = tokensParts;
          if (!header)
              return false;
          // @ts-ignore
          const parsedHeader = JSON.parse(atob(header));
          if ("typ" in parsedHeader && parsedHeader?.typ !== "JWT")
              return false;
          if (!parsedHeader.alg)
              return false;
          if (algorithm && (!("alg" in parsedHeader) || parsedHeader.alg !== algorithm))
              return false;
          return true;
      }
      catch {
          return false;
      }
  }
  const $ZodJWT = /*@__PURE__*/ $constructor("$ZodJWT", (inst, def) => {
      $ZodStringFormat.init(inst, def);
      inst._zod.check = (payload) => {
          if (isValidJWT(payload.value, def.alg))
              return;
          payload.issues.push({
              code: "invalid_format",
              format: "jwt",
              input: payload.value,
              inst,
              continue: !def.abort,
          });
      };
  });
  const $ZodNumber = /*@__PURE__*/ $constructor("$ZodNumber", (inst, def) => {
      $ZodType.init(inst, def);
      inst._zod.pattern = inst._zod.bag.pattern ?? number$1;
      inst._zod.parse = (payload, _ctx) => {
          if (def.coerce)
              try {
                  payload.value = Number(payload.value);
              }
              catch (_) { }
          const input = payload.value;
          if (typeof input === "number" && !Number.isNaN(input) && Number.isFinite(input)) {
              return payload;
          }
          const received = typeof input === "number"
              ? Number.isNaN(input)
                  ? "NaN"
                  : !Number.isFinite(input)
                      ? "Infinity"
                      : undefined
              : undefined;
          payload.issues.push({
              expected: "number",
              code: "invalid_type",
              input,
              inst,
              ...(received ? { received } : {}),
          });
          return payload;
      };
  });
  const $ZodNumberFormat = /*@__PURE__*/ $constructor("$ZodNumberFormat", (inst, def) => {
      $ZodCheckNumberFormat.init(inst, def);
      $ZodNumber.init(inst, def); // no format checks
  });
  const $ZodBoolean = /*@__PURE__*/ $constructor("$ZodBoolean", (inst, def) => {
      $ZodType.init(inst, def);
      inst._zod.pattern = boolean$1;
      inst._zod.parse = (payload, _ctx) => {
          if (def.coerce)
              try {
                  payload.value = Boolean(payload.value);
              }
              catch (_) { }
          const input = payload.value;
          if (typeof input === "boolean")
              return payload;
          payload.issues.push({
              expected: "boolean",
              code: "invalid_type",
              input,
              inst,
          });
          return payload;
      };
  });
  const $ZodAny = /*@__PURE__*/ $constructor("$ZodAny", (inst, def) => {
      $ZodType.init(inst, def);
      inst._zod.parse = (payload) => payload;
  });
  const $ZodUnknown = /*@__PURE__*/ $constructor("$ZodUnknown", (inst, def) => {
      $ZodType.init(inst, def);
      inst._zod.parse = (payload) => payload;
  });
  const $ZodNever = /*@__PURE__*/ $constructor("$ZodNever", (inst, def) => {
      $ZodType.init(inst, def);
      inst._zod.parse = (payload, _ctx) => {
          payload.issues.push({
              expected: "never",
              code: "invalid_type",
              input: payload.value,
              inst,
          });
          return payload;
      };
  });
  function handleArrayResult(result, final, index) {
      if (result.issues.length) {
          final.issues.push(...prefixIssues(index, result.issues));
      }
      final.value[index] = result.value;
  }
  const $ZodArray = /*@__PURE__*/ $constructor("$ZodArray", (inst, def) => {
      $ZodType.init(inst, def);
      inst._zod.parse = (payload, ctx) => {
          const input = payload.value;
          if (!Array.isArray(input)) {
              payload.issues.push({
                  expected: "array",
                  code: "invalid_type",
                  input,
                  inst,
              });
              return payload;
          }
          payload.value = Array(input.length);
          const proms = [];
          for (let i = 0; i < input.length; i++) {
              const item = input[i];
              const result = def.element._zod.run({
                  value: item,
                  issues: [],
              }, ctx);
              if (result instanceof Promise) {
                  proms.push(result.then((result) => handleArrayResult(result, payload, i)));
              }
              else {
                  handleArrayResult(result, payload, i);
              }
          }
          if (proms.length) {
              return Promise.all(proms).then(() => payload);
          }
          return payload; //handleArrayResultsAsync(parseResults, final);
      };
  });
  function handlePropertyResult(result, final, key, input) {
      if (result.issues.length) {
          final.issues.push(...prefixIssues(key, result.issues));
      }
      if (result.value === undefined) {
          if (key in input) {
              final.value[key] = undefined;
          }
      }
      else {
          final.value[key] = result.value;
      }
  }
  function normalizeDef(def) {
      const keys = Object.keys(def.shape);
      for (const k of keys) {
          if (!def.shape?.[k]?._zod?.traits?.has("$ZodType")) {
              throw new Error(`Invalid element at key "${k}": expected a Zod schema`);
          }
      }
      const okeys = optionalKeys(def.shape);
      return {
          ...def,
          keys,
          keySet: new Set(keys),
          numKeys: keys.length,
          optionalKeys: new Set(okeys),
      };
  }
  function handleCatchall(proms, input, payload, ctx, def, inst) {
      const unrecognized = [];
      // iterate over input keys
      const keySet = def.keySet;
      const _catchall = def.catchall._zod;
      const t = _catchall.def.type;
      for (const key in input) {
          if (keySet.has(key))
              continue;
          if (t === "never") {
              unrecognized.push(key);
              continue;
          }
          const r = _catchall.run({ value: input[key], issues: [] }, ctx);
          if (r instanceof Promise) {
              proms.push(r.then((r) => handlePropertyResult(r, payload, key, input)));
          }
          else {
              handlePropertyResult(r, payload, key, input);
          }
      }
      if (unrecognized.length) {
          payload.issues.push({
              code: "unrecognized_keys",
              keys: unrecognized,
              input,
              inst,
          });
      }
      if (!proms.length)
          return payload;
      return Promise.all(proms).then(() => {
          return payload;
      });
  }
  const $ZodObject = /*@__PURE__*/ $constructor("$ZodObject", (inst, def) => {
      // requires cast because technically $ZodObject doesn't extend
      $ZodType.init(inst, def);
      // const sh = def.shape;
      const desc = Object.getOwnPropertyDescriptor(def, "shape");
      if (!desc?.get) {
          const sh = def.shape;
          Object.defineProperty(def, "shape", {
              get: () => {
                  const newSh = { ...sh };
                  Object.defineProperty(def, "shape", {
                      value: newSh,
                  });
                  return newSh;
              },
          });
      }
      const _normalized = cached(() => normalizeDef(def));
      defineLazy(inst._zod, "propValues", () => {
          const shape = def.shape;
          const propValues = {};
          for (const key in shape) {
              const field = shape[key]._zod;
              if (field.values) {
                  propValues[key] ?? (propValues[key] = new Set());
                  for (const v of field.values)
                      propValues[key].add(v);
              }
          }
          return propValues;
      });
      const isObject$1 = isObject;
      const catchall = def.catchall;
      let value;
      inst._zod.parse = (payload, ctx) => {
          value ?? (value = _normalized.value);
          const input = payload.value;
          if (!isObject$1(input)) {
              payload.issues.push({
                  expected: "object",
                  code: "invalid_type",
                  input,
                  inst,
              });
              return payload;
          }
          payload.value = {};
          const proms = [];
          const shape = value.shape;
          for (const key of value.keys) {
              const el = shape[key];
              const r = el._zod.run({ value: input[key], issues: [] }, ctx);
              if (r instanceof Promise) {
                  proms.push(r.then((r) => handlePropertyResult(r, payload, key, input)));
              }
              else {
                  handlePropertyResult(r, payload, key, input);
              }
          }
          if (!catchall) {
              return proms.length ? Promise.all(proms).then(() => payload) : payload;
          }
          return handleCatchall(proms, input, payload, ctx, _normalized.value, inst);
      };
  });
  const $ZodObjectJIT = /*@__PURE__*/ $constructor("$ZodObjectJIT", (inst, def) => {
      // requires cast because technically $ZodObject doesn't extend
      $ZodObject.init(inst, def);
      const superParse = inst._zod.parse;
      const _normalized = cached(() => normalizeDef(def));
      const generateFastpass = (shape) => {
          const doc = new Doc(["shape", "payload", "ctx"]);
          const normalized = _normalized.value;
          const parseStr = (key) => {
              const k = esc(key);
              return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;
          };
          doc.write(`const input = payload.value;`);
          const ids = Object.create(null);
          let counter = 0;
          for (const key of normalized.keys) {
              ids[key] = `key_${counter++}`;
          }
          // A: preserve key order {
          doc.write(`const newResult = {};`);
          for (const key of normalized.keys) {
              const id = ids[key];
              const k = esc(key);
              doc.write(`const ${id} = ${parseStr(key)};`);
              doc.write(`
        if (${id}.issues.length) {
          payload.issues = payload.issues.concat(${id}.issues.map(iss => ({
            ...iss,
            path: iss.path ? [${k}, ...iss.path] : [${k}]
          })));
        }
        
        
        if (${id}.value === undefined) {
          if (${k} in input) {
            newResult[${k}] = undefined;
          }
        } else {
          newResult[${k}] = ${id}.value;
        }
        
      `);
          }
          doc.write(`payload.value = newResult;`);
          doc.write(`return payload;`);
          const fn = doc.compile();
          return (payload, ctx) => fn(shape, payload, ctx);
      };
      let fastpass;
      const isObject$1 = isObject;
      const jit = !globalConfig.jitless;
      const allowsEval$1 = allowsEval;
      const fastEnabled = jit && allowsEval$1.value; // && !def.catchall;
      const catchall = def.catchall;
      let value;
      inst._zod.parse = (payload, ctx) => {
          value ?? (value = _normalized.value);
          const input = payload.value;
          if (!isObject$1(input)) {
              payload.issues.push({
                  expected: "object",
                  code: "invalid_type",
                  input,
                  inst,
              });
              return payload;
          }
          if (jit && fastEnabled && ctx?.async === false && ctx.jitless !== true) {
              // always synchronous
              if (!fastpass)
                  fastpass = generateFastpass(def.shape);
              payload = fastpass(payload, ctx);
              if (!catchall)
                  return payload;
              return handleCatchall([], input, payload, ctx, value, inst);
          }
          return superParse(payload, ctx);
      };
  });
  function handleUnionResults(results, final, inst, ctx) {
      for (const result of results) {
          if (result.issues.length === 0) {
              final.value = result.value;
              return final;
          }
      }
      const nonaborted = results.filter((r) => !aborted(r));
      if (nonaborted.length === 1) {
          final.value = nonaborted[0].value;
          return nonaborted[0];
      }
      final.issues.push({
          code: "invalid_union",
          input: final.value,
          inst,
          errors: results.map((result) => result.issues.map((iss) => finalizeIssue(iss, ctx, config()))),
      });
      return final;
  }
  const $ZodUnion = /*@__PURE__*/ $constructor("$ZodUnion", (inst, def) => {
      $ZodType.init(inst, def);
      defineLazy(inst._zod, "optin", () => def.options.some((o) => o._zod.optin === "optional") ? "optional" : undefined);
      defineLazy(inst._zod, "optout", () => def.options.some((o) => o._zod.optout === "optional") ? "optional" : undefined);
      defineLazy(inst._zod, "values", () => {
          if (def.options.every((o) => o._zod.values)) {
              return new Set(def.options.flatMap((option) => Array.from(option._zod.values)));
          }
          return undefined;
      });
      defineLazy(inst._zod, "pattern", () => {
          if (def.options.every((o) => o._zod.pattern)) {
              const patterns = def.options.map((o) => o._zod.pattern);
              return new RegExp(`^(${patterns.map((p) => cleanRegex(p.source)).join("|")})$`);
          }
          return undefined;
      });
      const single = def.options.length === 1;
      const first = def.options[0]._zod.run;
      inst._zod.parse = (payload, ctx) => {
          if (single) {
              return first(payload, ctx);
          }
          let async = false;
          const results = [];
          for (const option of def.options) {
              const result = option._zod.run({
                  value: payload.value,
                  issues: [],
              }, ctx);
              if (result instanceof Promise) {
                  results.push(result);
                  async = true;
              }
              else {
                  if (result.issues.length === 0)
                      return result;
                  results.push(result);
              }
          }
          if (!async)
              return handleUnionResults(results, payload, inst, ctx);
          return Promise.all(results).then((results) => {
              return handleUnionResults(results, payload, inst, ctx);
          });
      };
  });
  const $ZodIntersection = /*@__PURE__*/ $constructor("$ZodIntersection", (inst, def) => {
      $ZodType.init(inst, def);
      inst._zod.parse = (payload, ctx) => {
          const input = payload.value;
          const left = def.left._zod.run({ value: input, issues: [] }, ctx);
          const right = def.right._zod.run({ value: input, issues: [] }, ctx);
          const async = left instanceof Promise || right instanceof Promise;
          if (async) {
              return Promise.all([left, right]).then(([left, right]) => {
                  return handleIntersectionResults(payload, left, right);
              });
          }
          return handleIntersectionResults(payload, left, right);
      };
  });
  function mergeValues(a, b) {
      // const aType = parse.t(a);
      // const bType = parse.t(b);
      if (a === b) {
          return { valid: true, data: a };
      }
      if (a instanceof Date && b instanceof Date && +a === +b) {
          return { valid: true, data: a };
      }
      if (isPlainObject(a) && isPlainObject(b)) {
          const bKeys = Object.keys(b);
          const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
          const newObj = { ...a, ...b };
          for (const key of sharedKeys) {
              const sharedValue = mergeValues(a[key], b[key]);
              if (!sharedValue.valid) {
                  return {
                      valid: false,
                      mergeErrorPath: [key, ...sharedValue.mergeErrorPath],
                  };
              }
              newObj[key] = sharedValue.data;
          }
          return { valid: true, data: newObj };
      }
      if (Array.isArray(a) && Array.isArray(b)) {
          if (a.length !== b.length) {
              return { valid: false, mergeErrorPath: [] };
          }
          const newArray = [];
          for (let index = 0; index < a.length; index++) {
              const itemA = a[index];
              const itemB = b[index];
              const sharedValue = mergeValues(itemA, itemB);
              if (!sharedValue.valid) {
                  return {
                      valid: false,
                      mergeErrorPath: [index, ...sharedValue.mergeErrorPath],
                  };
              }
              newArray.push(sharedValue.data);
          }
          return { valid: true, data: newArray };
      }
      return { valid: false, mergeErrorPath: [] };
  }
  function handleIntersectionResults(result, left, right) {
      if (left.issues.length) {
          result.issues.push(...left.issues);
      }
      if (right.issues.length) {
          result.issues.push(...right.issues);
      }
      if (aborted(result))
          return result;
      const merged = mergeValues(left.value, right.value);
      if (!merged.valid) {
          throw new Error(`Unmergable intersection. Error path: ` + `${JSON.stringify(merged.mergeErrorPath)}`);
      }
      result.value = merged.data;
      return result;
  }
  const $ZodTuple = /*@__PURE__*/ $constructor("$ZodTuple", (inst, def) => {
      $ZodType.init(inst, def);
      const items = def.items;
      inst._zod.parse = (payload, ctx) => {
          const input = payload.value;
          if (!Array.isArray(input)) {
              payload.issues.push({
                  input,
                  inst,
                  expected: "tuple",
                  code: "invalid_type",
              });
              return payload;
          }
          payload.value = [];
          const proms = [];
          const reversedIndex = [...items].reverse().findIndex((item) => item._zod.optin !== "optional");
          const optStart = reversedIndex === -1 ? 0 : items.length - reversedIndex;
          if (!def.rest) {
              const tooBig = input.length > items.length;
              const tooSmall = input.length < optStart - 1;
              if (tooBig || tooSmall) {
                  payload.issues.push({
                      ...(tooBig ? { code: "too_big", maximum: items.length } : { code: "too_small", minimum: items.length }),
                      input,
                      inst,
                      origin: "array",
                  });
                  return payload;
              }
          }
          let i = -1;
          for (const item of items) {
              i++;
              if (i >= input.length)
                  if (i >= optStart)
                      continue;
              const result = item._zod.run({
                  value: input[i],
                  issues: [],
              }, ctx);
              if (result instanceof Promise) {
                  proms.push(result.then((result) => handleTupleResult(result, payload, i)));
              }
              else {
                  handleTupleResult(result, payload, i);
              }
          }
          if (def.rest) {
              const rest = input.slice(items.length);
              for (const el of rest) {
                  i++;
                  const result = def.rest._zod.run({
                      value: el,
                      issues: [],
                  }, ctx);
                  if (result instanceof Promise) {
                      proms.push(result.then((result) => handleTupleResult(result, payload, i)));
                  }
                  else {
                      handleTupleResult(result, payload, i);
                  }
              }
          }
          if (proms.length)
              return Promise.all(proms).then(() => payload);
          return payload;
      };
  });
  function handleTupleResult(result, final, index) {
      if (result.issues.length) {
          final.issues.push(...prefixIssues(index, result.issues));
      }
      final.value[index] = result.value;
  }
  const $ZodEnum = /*@__PURE__*/ $constructor("$ZodEnum", (inst, def) => {
      $ZodType.init(inst, def);
      const values = getEnumValues(def.entries);
      const valuesSet = new Set(values);
      inst._zod.values = valuesSet;
      inst._zod.pattern = new RegExp(`^(${values
        .filter((k) => propertyKeyTypes.has(typeof k))
        .map((o) => (typeof o === "string" ? escapeRegex(o) : o.toString()))
        .join("|")})$`);
      inst._zod.parse = (payload, _ctx) => {
          const input = payload.value;
          if (valuesSet.has(input)) {
              return payload;
          }
          payload.issues.push({
              code: "invalid_value",
              values,
              input,
              inst,
          });
          return payload;
      };
  });
  const $ZodLiteral = /*@__PURE__*/ $constructor("$ZodLiteral", (inst, def) => {
      $ZodType.init(inst, def);
      if (def.values.length === 0) {
          throw new Error("Cannot create literal schema with no valid values");
      }
      const values = new Set(def.values);
      inst._zod.values = values;
      inst._zod.pattern = new RegExp(`^(${def.values
        .map((o) => (typeof o === "string" ? escapeRegex(o) : o ? escapeRegex(o.toString()) : String(o)))
        .join("|")})$`);
      inst._zod.parse = (payload, _ctx) => {
          const input = payload.value;
          if (values.has(input)) {
              return payload;
          }
          payload.issues.push({
              code: "invalid_value",
              values: def.values,
              input,
              inst,
          });
          return payload;
      };
  });
  const $ZodTransform = /*@__PURE__*/ $constructor("$ZodTransform", (inst, def) => {
      $ZodType.init(inst, def);
      inst._zod.parse = (payload, ctx) => {
          if (ctx.direction === "backward") {
              throw new $ZodEncodeError(inst.constructor.name);
          }
          const _out = def.transform(payload.value, payload);
          if (ctx.async) {
              const output = _out instanceof Promise ? _out : Promise.resolve(_out);
              return output.then((output) => {
                  payload.value = output;
                  return payload;
              });
          }
          if (_out instanceof Promise) {
              throw new $ZodAsyncError();
          }
          payload.value = _out;
          return payload;
      };
  });
  function handleOptionalResult(result, input) {
      if (result.issues.length && input === undefined) {
          return { issues: [], value: undefined };
      }
      return result;
  }
  const $ZodOptional = /*@__PURE__*/ $constructor("$ZodOptional", (inst, def) => {
      $ZodType.init(inst, def);
      inst._zod.optin = "optional";
      inst._zod.optout = "optional";
      defineLazy(inst._zod, "values", () => {
          return def.innerType._zod.values ? new Set([...def.innerType._zod.values, undefined]) : undefined;
      });
      defineLazy(inst._zod, "pattern", () => {
          const pattern = def.innerType._zod.pattern;
          return pattern ? new RegExp(`^(${cleanRegex(pattern.source)})?$`) : undefined;
      });
      inst._zod.parse = (payload, ctx) => {
          if (def.innerType._zod.optin === "optional") {
              const result = def.innerType._zod.run(payload, ctx);
              if (result instanceof Promise)
                  return result.then((r) => handleOptionalResult(r, payload.value));
              return handleOptionalResult(result, payload.value);
          }
          if (payload.value === undefined) {
              return payload;
          }
          return def.innerType._zod.run(payload, ctx);
      };
  });
  const $ZodNullable = /*@__PURE__*/ $constructor("$ZodNullable", (inst, def) => {
      $ZodType.init(inst, def);
      defineLazy(inst._zod, "optin", () => def.innerType._zod.optin);
      defineLazy(inst._zod, "optout", () => def.innerType._zod.optout);
      defineLazy(inst._zod, "pattern", () => {
          const pattern = def.innerType._zod.pattern;
          return pattern ? new RegExp(`^(${cleanRegex(pattern.source)}|null)$`) : undefined;
      });
      defineLazy(inst._zod, "values", () => {
          return def.innerType._zod.values ? new Set([...def.innerType._zod.values, null]) : undefined;
      });
      inst._zod.parse = (payload, ctx) => {
          // Forward direction (decode): allow null to pass through
          if (payload.value === null)
              return payload;
          return def.innerType._zod.run(payload, ctx);
      };
  });
  const $ZodDefault = /*@__PURE__*/ $constructor("$ZodDefault", (inst, def) => {
      $ZodType.init(inst, def);
      // inst._zod.qin = "true";
      inst._zod.optin = "optional";
      defineLazy(inst._zod, "values", () => def.innerType._zod.values);
      inst._zod.parse = (payload, ctx) => {
          if (ctx.direction === "backward") {
              return def.innerType._zod.run(payload, ctx);
          }
          // Forward direction (decode): apply defaults for undefined input
          if (payload.value === undefined) {
              payload.value = def.defaultValue;
              /**
               * $ZodDefault returns the default value immediately in forward direction.
               * It doesn't pass the default value into the validator ("prefault"). There's no reason to pass the default value through validation. The validity of the default is enforced by TypeScript statically. Otherwise, it's the responsibility of the user to ensure the default is valid. In the case of pipes with divergent in/out types, you can specify the default on the `in` schema of your ZodPipe to set a "prefault" for the pipe.   */
              return payload;
          }
          // Forward direction: continue with default handling
          const result = def.innerType._zod.run(payload, ctx);
          if (result instanceof Promise) {
              return result.then((result) => handleDefaultResult(result, def));
          }
          return handleDefaultResult(result, def);
      };
  });
  function handleDefaultResult(payload, def) {
      if (payload.value === undefined) {
          payload.value = def.defaultValue;
      }
      return payload;
  }
  const $ZodPrefault = /*@__PURE__*/ $constructor("$ZodPrefault", (inst, def) => {
      $ZodType.init(inst, def);
      inst._zod.optin = "optional";
      defineLazy(inst._zod, "values", () => def.innerType._zod.values);
      inst._zod.parse = (payload, ctx) => {
          if (ctx.direction === "backward") {
              return def.innerType._zod.run(payload, ctx);
          }
          // Forward direction (decode): apply prefault for undefined input
          if (payload.value === undefined) {
              payload.value = def.defaultValue;
          }
          return def.innerType._zod.run(payload, ctx);
      };
  });
  const $ZodNonOptional = /*@__PURE__*/ $constructor("$ZodNonOptional", (inst, def) => {
      $ZodType.init(inst, def);
      defineLazy(inst._zod, "values", () => {
          const v = def.innerType._zod.values;
          return v ? new Set([...v].filter((x) => x !== undefined)) : undefined;
      });
      inst._zod.parse = (payload, ctx) => {
          const result = def.innerType._zod.run(payload, ctx);
          if (result instanceof Promise) {
              return result.then((result) => handleNonOptionalResult(result, inst));
          }
          return handleNonOptionalResult(result, inst);
      };
  });
  function handleNonOptionalResult(payload, inst) {
      if (!payload.issues.length && payload.value === undefined) {
          payload.issues.push({
              code: "invalid_type",
              expected: "nonoptional",
              input: payload.value,
              inst,
          });
      }
      return payload;
  }
  const $ZodCatch = /*@__PURE__*/ $constructor("$ZodCatch", (inst, def) => {
      $ZodType.init(inst, def);
      defineLazy(inst._zod, "optin", () => def.innerType._zod.optin);
      defineLazy(inst._zod, "optout", () => def.innerType._zod.optout);
      defineLazy(inst._zod, "values", () => def.innerType._zod.values);
      inst._zod.parse = (payload, ctx) => {
          if (ctx.direction === "backward") {
              return def.innerType._zod.run(payload, ctx);
          }
          // Forward direction (decode): apply catch logic
          const result = def.innerType._zod.run(payload, ctx);
          if (result instanceof Promise) {
              return result.then((result) => {
                  payload.value = result.value;
                  if (result.issues.length) {
                      payload.value = def.catchValue({
                          ...payload,
                          error: {
                              issues: result.issues.map((iss) => finalizeIssue(iss, ctx, config())),
                          },
                          input: payload.value,
                      });
                      payload.issues = [];
                  }
                  return payload;
              });
          }
          payload.value = result.value;
          if (result.issues.length) {
              payload.value = def.catchValue({
                  ...payload,
                  error: {
                      issues: result.issues.map((iss) => finalizeIssue(iss, ctx, config())),
                  },
                  input: payload.value,
              });
              payload.issues = [];
          }
          return payload;
      };
  });
  const $ZodPipe = /*@__PURE__*/ $constructor("$ZodPipe", (inst, def) => {
      $ZodType.init(inst, def);
      defineLazy(inst._zod, "values", () => def.in._zod.values);
      defineLazy(inst._zod, "optin", () => def.in._zod.optin);
      defineLazy(inst._zod, "optout", () => def.out._zod.optout);
      defineLazy(inst._zod, "propValues", () => def.in._zod.propValues);
      inst._zod.parse = (payload, ctx) => {
          if (ctx.direction === "backward") {
              const right = def.out._zod.run(payload, ctx);
              if (right instanceof Promise) {
                  return right.then((right) => handlePipeResult(right, def.in, ctx));
              }
              return handlePipeResult(right, def.in, ctx);
          }
          const left = def.in._zod.run(payload, ctx);
          if (left instanceof Promise) {
              return left.then((left) => handlePipeResult(left, def.out, ctx));
          }
          return handlePipeResult(left, def.out, ctx);
      };
  });
  function handlePipeResult(left, next, ctx) {
      if (left.issues.length) {
          // prevent further checks
          left.aborted = true;
          return left;
      }
      return next._zod.run({ value: left.value, issues: left.issues }, ctx);
  }
  const $ZodReadonly = /*@__PURE__*/ $constructor("$ZodReadonly", (inst, def) => {
      $ZodType.init(inst, def);
      defineLazy(inst._zod, "propValues", () => def.innerType._zod.propValues);
      defineLazy(inst._zod, "values", () => def.innerType._zod.values);
      defineLazy(inst._zod, "optin", () => def.innerType?._zod?.optin);
      defineLazy(inst._zod, "optout", () => def.innerType?._zod?.optout);
      inst._zod.parse = (payload, ctx) => {
          if (ctx.direction === "backward") {
              return def.innerType._zod.run(payload, ctx);
          }
          const result = def.innerType._zod.run(payload, ctx);
          if (result instanceof Promise) {
              return result.then(handleReadonlyResult);
          }
          return handleReadonlyResult(result);
      };
  });
  function handleReadonlyResult(payload) {
      payload.value = Object.freeze(payload.value);
      return payload;
  }
  const $ZodFunction = /*@__PURE__*/ $constructor("$ZodFunction", (inst, def) => {
      $ZodType.init(inst, def);
      inst._def = def;
      inst._zod.def = def;
      inst.implement = (func) => {
          if (typeof func !== "function") {
              throw new Error("implement() must be called with a function");
          }
          return function (...args) {
              const parsedArgs = inst._def.input ? parse$2(inst._def.input, args) : args;
              const result = Reflect.apply(func, this, parsedArgs);
              if (inst._def.output) {
                  return parse$2(inst._def.output, result);
              }
              return result;
          };
      };
      inst.implementAsync = (func) => {
          if (typeof func !== "function") {
              throw new Error("implementAsync() must be called with a function");
          }
          return async function (...args) {
              const parsedArgs = inst._def.input ? await parseAsync$1(inst._def.input, args) : args;
              const result = await Reflect.apply(func, this, parsedArgs);
              if (inst._def.output) {
                  return await parseAsync$1(inst._def.output, result);
              }
              return result;
          };
      };
      inst._zod.parse = (payload, _ctx) => {
          if (typeof payload.value !== "function") {
              payload.issues.push({
                  code: "invalid_type",
                  expected: "function",
                  input: payload.value,
                  inst,
              });
              return payload;
          }
          // Check if output is a promise type to determine if we should use async implementation
          const hasPromiseOutput = inst._def.output && inst._def.output._zod.def.type === "promise";
          if (hasPromiseOutput) {
              payload.value = inst.implementAsync(payload.value);
          }
          else {
              payload.value = inst.implement(payload.value);
          }
          return payload;
      };
      inst.input = (...args) => {
          const F = inst.constructor;
          if (Array.isArray(args[0])) {
              return new F({
                  type: "function",
                  input: new $ZodTuple({
                      type: "tuple",
                      items: args[0],
                      rest: args[1],
                  }),
                  output: inst._def.output,
              });
          }
          return new F({
              type: "function",
              input: args[0],
              output: inst._def.output,
          });
      };
      inst.output = (output) => {
          const F = inst.constructor;
          return new F({
              type: "function",
              input: inst._def.input,
              output,
          });
      };
      return inst;
  });
  const $ZodCustom = /*@__PURE__*/ $constructor("$ZodCustom", (inst, def) => {
      $ZodCheck.init(inst, def);
      $ZodType.init(inst, def);
      inst._zod.parse = (payload, _) => {
          return payload;
      };
      inst._zod.check = (payload) => {
          const input = payload.value;
          const r = def.fn(input);
          if (r instanceof Promise) {
              return r.then((r) => handleRefineResult(r, payload, input, inst));
          }
          handleRefineResult(r, payload, input, inst);
          return;
      };
  });
  function handleRefineResult(result, payload, input, inst) {
      if (!result) {
          const _iss = {
              code: "custom",
              input,
              inst, // incorporates params.error into issue reporting
              path: [...(inst._zod.def.path ?? [])], // incorporates params.error into issue reporting
              continue: !inst._zod.def.abort,
              // params: inst._zod.def.params,
          };
          if (inst._zod.def.params)
              _iss.params = inst._zod.def.params;
          payload.issues.push(issue(_iss));
      }
  }

  var _a;
  class $ZodRegistry {
      constructor() {
          this._map = new WeakMap();
          this._idmap = new Map();
      }
      add(schema, ..._meta) {
          const meta = _meta[0];
          this._map.set(schema, meta);
          if (meta && typeof meta === "object" && "id" in meta) {
              if (this._idmap.has(meta.id)) {
                  throw new Error(`ID ${meta.id} already exists in the registry`);
              }
              this._idmap.set(meta.id, schema);
          }
          return this;
      }
      clear() {
          this._map = new WeakMap();
          this._idmap = new Map();
          return this;
      }
      remove(schema) {
          const meta = this._map.get(schema);
          if (meta && typeof meta === "object" && "id" in meta) {
              this._idmap.delete(meta.id);
          }
          this._map.delete(schema);
          return this;
      }
      get(schema) {
          // return this._map.get(schema) as any;
          // inherit metadata
          const p = schema._zod.parent;
          if (p) {
              const pm = { ...(this.get(p) ?? {}) };
              delete pm.id; // do not inherit id
              const f = { ...pm, ...this._map.get(schema) };
              return Object.keys(f).length ? f : undefined;
          }
          return this._map.get(schema);
      }
      has(schema) {
          return this._map.has(schema);
      }
  }
  // registries
  function registry() {
      return new $ZodRegistry();
  }
  (_a = globalThis).__zod_globalRegistry ?? (_a.__zod_globalRegistry = registry());
  const globalRegistry = globalThis.__zod_globalRegistry;

  function _string(Class, params) {
      return new Class({
          type: "string",
          ...normalizeParams(params),
      });
  }
  function _email(Class, params) {
      return new Class({
          type: "string",
          format: "email",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _guid(Class, params) {
      return new Class({
          type: "string",
          format: "guid",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _uuid(Class, params) {
      return new Class({
          type: "string",
          format: "uuid",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _uuidv4(Class, params) {
      return new Class({
          type: "string",
          format: "uuid",
          check: "string_format",
          abort: false,
          version: "v4",
          ...normalizeParams(params),
      });
  }
  function _uuidv6(Class, params) {
      return new Class({
          type: "string",
          format: "uuid",
          check: "string_format",
          abort: false,
          version: "v6",
          ...normalizeParams(params),
      });
  }
  function _uuidv7(Class, params) {
      return new Class({
          type: "string",
          format: "uuid",
          check: "string_format",
          abort: false,
          version: "v7",
          ...normalizeParams(params),
      });
  }
  function _url(Class, params) {
      return new Class({
          type: "string",
          format: "url",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _emoji(Class, params) {
      return new Class({
          type: "string",
          format: "emoji",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _nanoid(Class, params) {
      return new Class({
          type: "string",
          format: "nanoid",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _cuid(Class, params) {
      return new Class({
          type: "string",
          format: "cuid",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _cuid2(Class, params) {
      return new Class({
          type: "string",
          format: "cuid2",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _ulid(Class, params) {
      return new Class({
          type: "string",
          format: "ulid",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _xid(Class, params) {
      return new Class({
          type: "string",
          format: "xid",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _ksuid(Class, params) {
      return new Class({
          type: "string",
          format: "ksuid",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _ipv4(Class, params) {
      return new Class({
          type: "string",
          format: "ipv4",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _ipv6(Class, params) {
      return new Class({
          type: "string",
          format: "ipv6",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _cidrv4(Class, params) {
      return new Class({
          type: "string",
          format: "cidrv4",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _cidrv6(Class, params) {
      return new Class({
          type: "string",
          format: "cidrv6",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _base64(Class, params) {
      return new Class({
          type: "string",
          format: "base64",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _base64url(Class, params) {
      return new Class({
          type: "string",
          format: "base64url",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _e164(Class, params) {
      return new Class({
          type: "string",
          format: "e164",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _jwt(Class, params) {
      return new Class({
          type: "string",
          format: "jwt",
          check: "string_format",
          abort: false,
          ...normalizeParams(params),
      });
  }
  function _isoDateTime(Class, params) {
      return new Class({
          type: "string",
          format: "datetime",
          check: "string_format",
          offset: false,
          local: false,
          precision: null,
          ...normalizeParams(params),
      });
  }
  function _isoDate(Class, params) {
      return new Class({
          type: "string",
          format: "date",
          check: "string_format",
          ...normalizeParams(params),
      });
  }
  function _isoTime(Class, params) {
      return new Class({
          type: "string",
          format: "time",
          check: "string_format",
          precision: null,
          ...normalizeParams(params),
      });
  }
  function _isoDuration(Class, params) {
      return new Class({
          type: "string",
          format: "duration",
          check: "string_format",
          ...normalizeParams(params),
      });
  }
  function _number(Class, params) {
      return new Class({
          type: "number",
          checks: [],
          ...normalizeParams(params),
      });
  }
  function _int(Class, params) {
      return new Class({
          type: "number",
          check: "number_format",
          abort: false,
          format: "safeint",
          ...normalizeParams(params),
      });
  }
  function _boolean(Class, params) {
      return new Class({
          type: "boolean",
          ...normalizeParams(params),
      });
  }
  function _any(Class) {
      return new Class({
          type: "any",
      });
  }
  function _unknown(Class) {
      return new Class({
          type: "unknown",
      });
  }
  function _never(Class, params) {
      return new Class({
          type: "never",
          ...normalizeParams(params),
      });
  }
  function _lt(value, params) {
      return new $ZodCheckLessThan({
          check: "less_than",
          ...normalizeParams(params),
          value,
          inclusive: false,
      });
  }
  function _lte(value, params) {
      return new $ZodCheckLessThan({
          check: "less_than",
          ...normalizeParams(params),
          value,
          inclusive: true,
      });
  }
  function _gt(value, params) {
      return new $ZodCheckGreaterThan({
          check: "greater_than",
          ...normalizeParams(params),
          value,
          inclusive: false,
      });
  }
  function _gte(value, params) {
      return new $ZodCheckGreaterThan({
          check: "greater_than",
          ...normalizeParams(params),
          value,
          inclusive: true,
      });
  }
  function _multipleOf(value, params) {
      return new $ZodCheckMultipleOf({
          check: "multiple_of",
          ...normalizeParams(params),
          value,
      });
  }
  function _maxLength(maximum, params) {
      const ch = new $ZodCheckMaxLength({
          check: "max_length",
          ...normalizeParams(params),
          maximum,
      });
      return ch;
  }
  function _minLength(minimum, params) {
      return new $ZodCheckMinLength({
          check: "min_length",
          ...normalizeParams(params),
          minimum,
      });
  }
  function _length(length, params) {
      return new $ZodCheckLengthEquals({
          check: "length_equals",
          ...normalizeParams(params),
          length,
      });
  }
  function _regex(pattern, params) {
      return new $ZodCheckRegex({
          check: "string_format",
          format: "regex",
          ...normalizeParams(params),
          pattern,
      });
  }
  function _lowercase(params) {
      return new $ZodCheckLowerCase({
          check: "string_format",
          format: "lowercase",
          ...normalizeParams(params),
      });
  }
  function _uppercase(params) {
      return new $ZodCheckUpperCase({
          check: "string_format",
          format: "uppercase",
          ...normalizeParams(params),
      });
  }
  function _includes(includes, params) {
      return new $ZodCheckIncludes({
          check: "string_format",
          format: "includes",
          ...normalizeParams(params),
          includes,
      });
  }
  function _startsWith(prefix, params) {
      return new $ZodCheckStartsWith({
          check: "string_format",
          format: "starts_with",
          ...normalizeParams(params),
          prefix,
      });
  }
  function _endsWith(suffix, params) {
      return new $ZodCheckEndsWith({
          check: "string_format",
          format: "ends_with",
          ...normalizeParams(params),
          suffix,
      });
  }
  function _overwrite(tx) {
      return new $ZodCheckOverwrite({
          check: "overwrite",
          tx,
      });
  }
  // normalize
  function _normalize(form) {
      return _overwrite((input) => input.normalize(form));
  }
  // trim
  function _trim() {
      return _overwrite((input) => input.trim());
  }
  // toLowerCase
  function _toLowerCase() {
      return _overwrite((input) => input.toLowerCase());
  }
  // toUpperCase
  function _toUpperCase() {
      return _overwrite((input) => input.toUpperCase());
  }
  // slugify
  function _slugify() {
      return _overwrite((input) => slugify(input));
  }
  function _array(Class, element, params) {
      return new Class({
          type: "array",
          element,
          // get element() {
          //   return element;
          // },
          ...normalizeParams(params),
      });
  }
  function _custom(Class, fn, _params) {
      const norm = normalizeParams(_params);
      norm.abort ?? (norm.abort = true); // default to abort:false
      const schema = new Class({
          type: "custom",
          check: "custom",
          fn: fn,
          ...norm,
      });
      return schema;
  }
  // same as _custom but defaults to abort:false
  function _refine(Class, fn, _params) {
      const schema = new Class({
          type: "custom",
          check: "custom",
          fn: fn,
          ...normalizeParams(_params),
      });
      return schema;
  }
  function _superRefine(fn) {
      const ch = _check((payload) => {
          payload.addIssue = (issue$1) => {
              if (typeof issue$1 === "string") {
                  payload.issues.push(issue(issue$1, payload.value, ch._zod.def));
              }
              else {
                  // for Zod 3 backwards compatibility
                  const _issue = issue$1;
                  if (_issue.fatal)
                      _issue.continue = false;
                  _issue.code ?? (_issue.code = "custom");
                  _issue.input ?? (_issue.input = payload.value);
                  _issue.inst ?? (_issue.inst = ch);
                  _issue.continue ?? (_issue.continue = !ch._zod.def.abort); // abort is always undefined, so this is always true...
                  payload.issues.push(issue(_issue));
              }
          };
          return fn(payload.value, payload);
      });
      return ch;
  }
  function _check(fn, params) {
      const ch = new $ZodCheck({
          check: "custom",
          ...normalizeParams(params),
      });
      ch._zod.check = fn;
      return ch;
  }

  // function initializeContext<T extends schemas.$ZodType>(inputs: JSONSchemaGeneratorParams<T>): ToJSONSchemaContext<T> {
  //   return {
  //     processor: inputs.processor,
  //     metadataRegistry: inputs.metadata ?? globalRegistry,
  //     target: inputs.target ?? "draft-2020-12",
  //     unrepresentable: inputs.unrepresentable ?? "throw",
  //   };
  // }
  function initializeContext(params) {
      // Normalize target: convert old non-hyphenated versions to hyphenated versions
      let target = params?.target ?? "draft-2020-12";
      if (target === "draft-4")
          target = "draft-04";
      if (target === "draft-7")
          target = "draft-07";
      return {
          processors: params.processors ?? {},
          metadataRegistry: params?.metadata ?? globalRegistry,
          target,
          unrepresentable: params?.unrepresentable ?? "throw",
          override: params?.override ?? (() => { }),
          io: params?.io ?? "output",
          counter: 0,
          seen: new Map(),
          cycles: params?.cycles ?? "ref",
          reused: params?.reused ?? "inline",
          external: params?.external ?? undefined,
      };
  }
  function process(schema, ctx, _params = { path: [], schemaPath: [] }) {
      var _a;
      const def = schema._zod.def;
      // check for schema in seens
      const seen = ctx.seen.get(schema);
      if (seen) {
          seen.count++;
          // check if cycle
          const isCycle = _params.schemaPath.includes(schema);
          if (isCycle) {
              seen.cycle = _params.path;
          }
          return seen.schema;
      }
      // initialize
      const result = { schema: {}, count: 1, cycle: undefined, path: _params.path };
      ctx.seen.set(schema, result);
      // custom method overrides default behavior
      const overrideSchema = schema._zod.toJSONSchema?.();
      if (overrideSchema) {
          result.schema = overrideSchema;
      }
      else {
          const params = {
              ..._params,
              schemaPath: [..._params.schemaPath, schema],
              path: _params.path,
          };
          const parent = schema._zod.parent;
          if (parent) {
              // schema was cloned from another schema
              result.ref = parent;
              process(parent, ctx, params);
              ctx.seen.get(parent).isParent = true;
          }
          else if (schema._zod.processJSONSchema) {
              schema._zod.processJSONSchema(ctx, result.schema, params);
          }
          else {
              const _json = result.schema;
              const processor = ctx.processors[def.type];
              if (!processor) {
                  throw new Error(`[toJSONSchema]: Non-representable type encountered: ${def.type}`);
              }
              processor(schema, ctx, _json, params);
          }
      }
      // metadata
      const meta = ctx.metadataRegistry.get(schema);
      if (meta)
          Object.assign(result.schema, meta);
      if (ctx.io === "input" && isTransforming(schema)) {
          // examples/defaults only apply to output type of pipe
          delete result.schema.examples;
          delete result.schema.default;
      }
      // set prefault as default
      if (ctx.io === "input" && result.schema._prefault)
          (_a = result.schema).default ?? (_a.default = result.schema._prefault);
      delete result.schema._prefault;
      // pulling fresh from ctx.seen in case it was overwritten
      const _result = ctx.seen.get(schema);
      return _result.schema;
  }
  function extractDefs(ctx, schema
  // params: EmitParams
  ) {
      // iterate over seen map;
      const root = ctx.seen.get(schema);
      if (!root)
          throw new Error("Unprocessed schema. This is a bug in Zod.");
      // returns a ref to the schema
      // defId will be empty if the ref points to an external schema (or #)
      const makeURI = (entry) => {
          // comparing the seen objects because sometimes
          // multiple schemas map to the same seen object.
          // e.g. lazy
          // external is configured
          const defsSegment = ctx.target === "draft-2020-12" ? "$defs" : "definitions";
          if (ctx.external) {
              const externalId = ctx.external.registry.get(entry[0])?.id; // ?? "__shared";// `__schema${ctx.counter++}`;
              // check if schema is in the external registry
              const uriGenerator = ctx.external.uri ?? ((id) => id);
              if (externalId) {
                  return { ref: uriGenerator(externalId) };
              }
              // otherwise, add to __shared
              const id = entry[1].defId ?? entry[1].schema.id ?? `schema${ctx.counter++}`;
              entry[1].defId = id; // set defId so it will be reused if needed
              return { defId: id, ref: `${uriGenerator("__shared")}#/${defsSegment}/${id}` };
          }
          if (entry[1] === root) {
              return { ref: "#" };
          }
          // self-contained schema
          const uriPrefix = `#`;
          const defUriPrefix = `${uriPrefix}/${defsSegment}/`;
          const defId = entry[1].schema.id ?? `__schema${ctx.counter++}`;
          return { defId, ref: defUriPrefix + defId };
      };
      // stored cached version in `def` property
      // remove all properties, set $ref
      const extractToDef = (entry) => {
          // if the schema is already a reference, do not extract it
          if (entry[1].schema.$ref) {
              return;
          }
          const seen = entry[1];
          const { ref, defId } = makeURI(entry);
          seen.def = { ...seen.schema };
          // defId won't be set if the schema is a reference to an external schema
          // or if the schema is the root schema
          if (defId)
              seen.defId = defId;
          // wipe away all properties except $ref
          const schema = seen.schema;
          for (const key in schema) {
              delete schema[key];
          }
          schema.$ref = ref;
      };
      // throw on cycles
      // break cycles
      if (ctx.cycles === "throw") {
          for (const entry of ctx.seen.entries()) {
              const seen = entry[1];
              if (seen.cycle) {
                  throw new Error("Cycle detected: " +
                      `#/${seen.cycle?.join("/")}/<root>` +
                      '\n\nSet the `cycles` parameter to `"ref"` to resolve cyclical schemas with defs.');
              }
          }
      }
      // extract schemas into $defs
      for (const entry of ctx.seen.entries()) {
          const seen = entry[1];
          // convert root schema to # $ref
          if (schema === entry[0]) {
              extractToDef(entry); // this has special handling for the root schema
              continue;
          }
          // extract schemas that are in the external registry
          if (ctx.external) {
              const ext = ctx.external.registry.get(entry[0])?.id;
              if (schema !== entry[0] && ext) {
                  extractToDef(entry);
                  continue;
              }
          }
          // extract schemas with `id` meta
          const id = ctx.metadataRegistry.get(entry[0])?.id;
          if (id) {
              extractToDef(entry);
              continue;
          }
          // break cycles
          if (seen.cycle) {
              // any
              extractToDef(entry);
              continue;
          }
          // extract reused schemas
          if (seen.count > 1) {
              if (ctx.reused === "ref") {
                  extractToDef(entry);
                  // biome-ignore lint:
                  continue;
              }
          }
      }
  }
  function finalize(ctx, schema) {
      //
      // iterate over seen map;
      const root = ctx.seen.get(schema);
      if (!root)
          throw new Error("Unprocessed schema. This is a bug in Zod.");
      // flatten _refs
      const flattenRef = (zodSchema) => {
          const seen = ctx.seen.get(zodSchema);
          const schema = seen.def ?? seen.schema;
          const _cached = { ...schema };
          // already seen
          if (seen.ref === null) {
              return;
          }
          // flatten ref if defined
          const ref = seen.ref;
          seen.ref = null; // prevent recursion
          if (ref) {
              flattenRef(ref);
              // merge referenced schema into current
              const refSchema = ctx.seen.get(ref).schema;
              if (refSchema.$ref && (ctx.target === "draft-07" || ctx.target === "draft-04" || ctx.target === "openapi-3.0")) {
                  schema.allOf = schema.allOf ?? [];
                  schema.allOf.push(refSchema);
              }
              else {
                  Object.assign(schema, refSchema);
                  Object.assign(schema, _cached); // prevent overwriting any fields in the original schema
              }
          }
          // execute overrides
          if (!seen.isParent)
              ctx.override({
                  zodSchema: zodSchema,
                  jsonSchema: schema,
                  path: seen.path ?? [],
              });
      };
      for (const entry of [...ctx.seen.entries()].reverse()) {
          flattenRef(entry[0]);
      }
      const result = {};
      if (ctx.target === "draft-2020-12") {
          result.$schema = "https://json-schema.org/draft/2020-12/schema";
      }
      else if (ctx.target === "draft-07") {
          result.$schema = "http://json-schema.org/draft-07/schema#";
      }
      else if (ctx.target === "draft-04") {
          result.$schema = "http://json-schema.org/draft-04/schema#";
      }
      else ;
      if (ctx.external?.uri) {
          const id = ctx.external.registry.get(schema)?.id;
          if (!id)
              throw new Error("Schema is missing an `id` property");
          result.$id = ctx.external.uri(id);
      }
      Object.assign(result, root.def ?? root.schema);
      // build defs object
      const defs = ctx.external?.defs ?? {};
      for (const entry of ctx.seen.entries()) {
          const seen = entry[1];
          if (seen.def && seen.defId) {
              defs[seen.defId] = seen.def;
          }
      }
      // set definitions in result
      if (ctx.external) ;
      else {
          if (Object.keys(defs).length > 0) {
              if (ctx.target === "draft-2020-12") {
                  result.$defs = defs;
              }
              else {
                  result.definitions = defs;
              }
          }
      }
      try {
          // this "finalizes" this schema and ensures all cycles are removed
          // each call to finalize() is functionally independent
          // though the seen map is shared
          const finalized = JSON.parse(JSON.stringify(result));
          Object.defineProperty(finalized, "~standard", {
              value: {
                  ...schema["~standard"],
                  jsonSchema: {
                      input: createStandardJSONSchemaMethod(schema, "input"),
                      output: createStandardJSONSchemaMethod(schema, "output"),
                  },
              },
              enumerable: false,
              writable: false,
          });
          return finalized;
      }
      catch (_err) {
          throw new Error("Error converting schema to JSON.");
      }
  }
  function isTransforming(_schema, _ctx) {
      const ctx = _ctx ?? { seen: new Set() };
      if (ctx.seen.has(_schema))
          return false;
      ctx.seen.add(_schema);
      const def = _schema._zod.def;
      if (def.type === "transform")
          return true;
      if (def.type === "array")
          return isTransforming(def.element, ctx);
      if (def.type === "set")
          return isTransforming(def.valueType, ctx);
      if (def.type === "lazy")
          return isTransforming(def.getter(), ctx);
      if (def.type === "promise" ||
          def.type === "optional" ||
          def.type === "nonoptional" ||
          def.type === "nullable" ||
          def.type === "readonly" ||
          def.type === "default" ||
          def.type === "prefault") {
          return isTransforming(def.innerType, ctx);
      }
      if (def.type === "intersection") {
          return isTransforming(def.left, ctx) || isTransforming(def.right, ctx);
      }
      if (def.type === "record" || def.type === "map") {
          return isTransforming(def.keyType, ctx) || isTransforming(def.valueType, ctx);
      }
      if (def.type === "pipe") {
          return isTransforming(def.in, ctx) || isTransforming(def.out, ctx);
      }
      if (def.type === "object") {
          for (const key in def.shape) {
              if (isTransforming(def.shape[key], ctx))
                  return true;
          }
          return false;
      }
      if (def.type === "union") {
          for (const option of def.options) {
              if (isTransforming(option, ctx))
                  return true;
          }
          return false;
      }
      if (def.type === "tuple") {
          for (const item of def.items) {
              if (isTransforming(item, ctx))
                  return true;
          }
          if (def.rest && isTransforming(def.rest, ctx))
              return true;
          return false;
      }
      return false;
  }
  /**
   * Creates a toJSONSchema method for a schema instance.
   * This encapsulates the logic of initializing context, processing, extracting defs, and finalizing.
   */
  const createToJSONSchemaMethod = (schema, processors = {}) => (params) => {
      const ctx = initializeContext({ ...params, processors });
      process(schema, ctx);
      extractDefs(ctx, schema);
      return finalize(ctx, schema);
  };
  const createStandardJSONSchemaMethod = (schema, io) => (params) => {
      const { libraryOptions, target } = params ?? {};
      const ctx = initializeContext({ ...(libraryOptions ?? {}), target, io, processors: {} });
      process(schema, ctx);
      extractDefs(ctx, schema);
      return finalize(ctx, schema);
  };

  const formatMap = {
      guid: "uuid",
      url: "uri",
      datetime: "date-time",
      json_string: "json-string",
      regex: "", // do not set
  };
  // ==================== SIMPLE TYPE PROCESSORS ====================
  const stringProcessor = (schema, ctx, _json, _params) => {
      const json = _json;
      json.type = "string";
      const { minimum, maximum, format, patterns, contentEncoding } = schema._zod
          .bag;
      if (typeof minimum === "number")
          json.minLength = minimum;
      if (typeof maximum === "number")
          json.maxLength = maximum;
      // custom pattern overrides format
      if (format) {
          json.format = formatMap[format] ?? format;
          if (json.format === "")
              delete json.format; // empty format is not valid
      }
      if (contentEncoding)
          json.contentEncoding = contentEncoding;
      if (patterns && patterns.size > 0) {
          const regexes = [...patterns];
          if (regexes.length === 1)
              json.pattern = regexes[0].source;
          else if (regexes.length > 1) {
              json.allOf = [
                  ...regexes.map((regex) => ({
                      ...(ctx.target === "draft-07" || ctx.target === "draft-04" || ctx.target === "openapi-3.0"
                          ? { type: "string" }
                          : {}),
                      pattern: regex.source,
                  })),
              ];
          }
      }
  };
  const numberProcessor = (schema, ctx, _json, _params) => {
      const json = _json;
      const { minimum, maximum, format, multipleOf, exclusiveMaximum, exclusiveMinimum } = schema._zod.bag;
      if (typeof format === "string" && format.includes("int"))
          json.type = "integer";
      else
          json.type = "number";
      if (typeof exclusiveMinimum === "number") {
          if (ctx.target === "draft-04" || ctx.target === "openapi-3.0") {
              json.minimum = exclusiveMinimum;
              json.exclusiveMinimum = true;
          }
          else {
              json.exclusiveMinimum = exclusiveMinimum;
          }
      }
      if (typeof minimum === "number") {
          json.minimum = minimum;
          if (typeof exclusiveMinimum === "number" && ctx.target !== "draft-04") {
              if (exclusiveMinimum >= minimum)
                  delete json.minimum;
              else
                  delete json.exclusiveMinimum;
          }
      }
      if (typeof exclusiveMaximum === "number") {
          if (ctx.target === "draft-04" || ctx.target === "openapi-3.0") {
              json.maximum = exclusiveMaximum;
              json.exclusiveMaximum = true;
          }
          else {
              json.exclusiveMaximum = exclusiveMaximum;
          }
      }
      if (typeof maximum === "number") {
          json.maximum = maximum;
          if (typeof exclusiveMaximum === "number" && ctx.target !== "draft-04") {
              if (exclusiveMaximum <= maximum)
                  delete json.maximum;
              else
                  delete json.exclusiveMaximum;
          }
      }
      if (typeof multipleOf === "number")
          json.multipleOf = multipleOf;
  };
  const booleanProcessor = (_schema, _ctx, json, _params) => {
      json.type = "boolean";
  };
  const neverProcessor = (_schema, _ctx, json, _params) => {
      json.not = {};
  };
  const anyProcessor = (_schema, _ctx, _json, _params) => {
      // empty schema accepts anything
  };
  const unknownProcessor = (_schema, _ctx, _json, _params) => {
      // empty schema accepts anything
  };
  const enumProcessor = (schema, _ctx, json, _params) => {
      const def = schema._zod.def;
      const values = getEnumValues(def.entries);
      // Number enums can have both string and number values
      if (values.every((v) => typeof v === "number"))
          json.type = "number";
      if (values.every((v) => typeof v === "string"))
          json.type = "string";
      json.enum = values;
  };
  const literalProcessor = (schema, ctx, json, _params) => {
      const def = schema._zod.def;
      const vals = [];
      for (const val of def.values) {
          if (val === undefined) {
              if (ctx.unrepresentable === "throw") {
                  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
              }
          }
          else if (typeof val === "bigint") {
              if (ctx.unrepresentable === "throw") {
                  throw new Error("BigInt literals cannot be represented in JSON Schema");
              }
              else {
                  vals.push(Number(val));
              }
          }
          else {
              vals.push(val);
          }
      }
      if (vals.length === 0) ;
      else if (vals.length === 1) {
          const val = vals[0];
          json.type = val === null ? "null" : typeof val;
          if (ctx.target === "draft-04" || ctx.target === "openapi-3.0") {
              json.enum = [val];
          }
          else {
              json.const = val;
          }
      }
      else {
          if (vals.every((v) => typeof v === "number"))
              json.type = "number";
          if (vals.every((v) => typeof v === "string"))
              json.type = "string";
          if (vals.every((v) => typeof v === "boolean"))
              json.type = "boolean";
          if (vals.every((v) => v === null))
              json.type = "null";
          json.enum = vals;
      }
  };
  const customProcessor = (_schema, ctx, _json, _params) => {
      if (ctx.unrepresentable === "throw") {
          throw new Error("Custom types cannot be represented in JSON Schema");
      }
  };
  const functionProcessor = (_schema, ctx, _json, _params) => {
      if (ctx.unrepresentable === "throw") {
          throw new Error("Function types cannot be represented in JSON Schema");
      }
  };
  const transformProcessor = (_schema, ctx, _json, _params) => {
      if (ctx.unrepresentable === "throw") {
          throw new Error("Transforms cannot be represented in JSON Schema");
      }
  };
  // ==================== COMPOSITE TYPE PROCESSORS ====================
  const arrayProcessor = (schema, ctx, _json, params) => {
      const json = _json;
      const def = schema._zod.def;
      const { minimum, maximum } = schema._zod.bag;
      if (typeof minimum === "number")
          json.minItems = minimum;
      if (typeof maximum === "number")
          json.maxItems = maximum;
      json.type = "array";
      json.items = process(def.element, ctx, { ...params, path: [...params.path, "items"] });
  };
  const objectProcessor = (schema, ctx, _json, params) => {
      const json = _json;
      const def = schema._zod.def;
      json.type = "object";
      json.properties = {};
      const shape = def.shape;
      for (const key in shape) {
          json.properties[key] = process(shape[key], ctx, {
              ...params,
              path: [...params.path, "properties", key],
          });
      }
      // required keys
      const allKeys = new Set(Object.keys(shape));
      const requiredKeys = new Set([...allKeys].filter((key) => {
          const v = def.shape[key]._zod;
          if (ctx.io === "input") {
              return v.optin === undefined;
          }
          else {
              return v.optout === undefined;
          }
      }));
      if (requiredKeys.size > 0) {
          json.required = Array.from(requiredKeys);
      }
      // catchall
      if (def.catchall?._zod.def.type === "never") {
          // strict
          json.additionalProperties = false;
      }
      else if (!def.catchall) {
          // regular
          if (ctx.io === "output")
              json.additionalProperties = false;
      }
      else if (def.catchall) {
          json.additionalProperties = process(def.catchall, ctx, {
              ...params,
              path: [...params.path, "additionalProperties"],
          });
      }
  };
  const unionProcessor = (schema, ctx, json, params) => {
      const def = schema._zod.def;
      // Exclusive unions (inclusive === false) use oneOf (exactly one match) instead of anyOf (one or more matches)
      // This includes both z.xor() and discriminated unions
      const isExclusive = def.inclusive === false;
      const options = def.options.map((x, i) => process(x, ctx, {
          ...params,
          path: [...params.path, isExclusive ? "oneOf" : "anyOf", i],
      }));
      if (isExclusive) {
          json.oneOf = options;
      }
      else {
          json.anyOf = options;
      }
  };
  const intersectionProcessor = (schema, ctx, json, params) => {
      const def = schema._zod.def;
      const a = process(def.left, ctx, {
          ...params,
          path: [...params.path, "allOf", 0],
      });
      const b = process(def.right, ctx, {
          ...params,
          path: [...params.path, "allOf", 1],
      });
      const isSimpleIntersection = (val) => "allOf" in val && Object.keys(val).length === 1;
      const allOf = [
          ...(isSimpleIntersection(a) ? a.allOf : [a]),
          ...(isSimpleIntersection(b) ? b.allOf : [b]),
      ];
      json.allOf = allOf;
  };
  const tupleProcessor = (schema, ctx, _json, params) => {
      const json = _json;
      const def = schema._zod.def;
      json.type = "array";
      const prefixPath = ctx.target === "draft-2020-12" ? "prefixItems" : "items";
      const restPath = ctx.target === "draft-2020-12" ? "items" : ctx.target === "openapi-3.0" ? "items" : "additionalItems";
      const prefixItems = def.items.map((x, i) => process(x, ctx, {
          ...params,
          path: [...params.path, prefixPath, i],
      }));
      const rest = def.rest
          ? process(def.rest, ctx, {
              ...params,
              path: [...params.path, restPath, ...(ctx.target === "openapi-3.0" ? [def.items.length] : [])],
          })
          : null;
      if (ctx.target === "draft-2020-12") {
          json.prefixItems = prefixItems;
          if (rest) {
              json.items = rest;
          }
      }
      else if (ctx.target === "openapi-3.0") {
          json.items = {
              anyOf: prefixItems,
          };
          if (rest) {
              json.items.anyOf.push(rest);
          }
          json.minItems = prefixItems.length;
          if (!rest) {
              json.maxItems = prefixItems.length;
          }
      }
      else {
          json.items = prefixItems;
          if (rest) {
              json.additionalItems = rest;
          }
      }
      // length
      const { minimum, maximum } = schema._zod.bag;
      if (typeof minimum === "number")
          json.minItems = minimum;
      if (typeof maximum === "number")
          json.maxItems = maximum;
  };
  const nullableProcessor = (schema, ctx, json, params) => {
      const def = schema._zod.def;
      const inner = process(def.innerType, ctx, params);
      const seen = ctx.seen.get(schema);
      if (ctx.target === "openapi-3.0") {
          seen.ref = def.innerType;
          json.nullable = true;
      }
      else {
          json.anyOf = [inner, { type: "null" }];
      }
  };
  const nonoptionalProcessor = (schema, ctx, _json, params) => {
      const def = schema._zod.def;
      process(def.innerType, ctx, params);
      const seen = ctx.seen.get(schema);
      seen.ref = def.innerType;
  };
  const defaultProcessor = (schema, ctx, json, params) => {
      const def = schema._zod.def;
      process(def.innerType, ctx, params);
      const seen = ctx.seen.get(schema);
      seen.ref = def.innerType;
      json.default = JSON.parse(JSON.stringify(def.defaultValue));
  };
  const prefaultProcessor = (schema, ctx, json, params) => {
      const def = schema._zod.def;
      process(def.innerType, ctx, params);
      const seen = ctx.seen.get(schema);
      seen.ref = def.innerType;
      if (ctx.io === "input")
          json._prefault = JSON.parse(JSON.stringify(def.defaultValue));
  };
  const catchProcessor = (schema, ctx, json, params) => {
      const def = schema._zod.def;
      process(def.innerType, ctx, params);
      const seen = ctx.seen.get(schema);
      seen.ref = def.innerType;
      let catchValue;
      try {
          catchValue = def.catchValue(undefined);
      }
      catch {
          throw new Error("Dynamic catch values are not supported in JSON Schema");
      }
      json.default = catchValue;
  };
  const pipeProcessor = (schema, ctx, _json, params) => {
      const def = schema._zod.def;
      const innerType = ctx.io === "input" ? (def.in._zod.def.type === "transform" ? def.out : def.in) : def.out;
      process(innerType, ctx, params);
      const seen = ctx.seen.get(schema);
      seen.ref = innerType;
  };
  const readonlyProcessor = (schema, ctx, json, params) => {
      const def = schema._zod.def;
      process(def.innerType, ctx, params);
      const seen = ctx.seen.get(schema);
      seen.ref = def.innerType;
      json.readOnly = true;
  };
  const optionalProcessor = (schema, ctx, _json, params) => {
      const def = schema._zod.def;
      process(def.innerType, ctx, params);
      const seen = ctx.seen.get(schema);
      seen.ref = def.innerType;
  };

  const ZodISODateTime = /*@__PURE__*/ $constructor("ZodISODateTime", (inst, def) => {
      $ZodISODateTime.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  function datetime(params) {
      return _isoDateTime(ZodISODateTime, params);
  }
  const ZodISODate = /*@__PURE__*/ $constructor("ZodISODate", (inst, def) => {
      $ZodISODate.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  function date(params) {
      return _isoDate(ZodISODate, params);
  }
  const ZodISOTime = /*@__PURE__*/ $constructor("ZodISOTime", (inst, def) => {
      $ZodISOTime.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  function time(params) {
      return _isoTime(ZodISOTime, params);
  }
  const ZodISODuration = /*@__PURE__*/ $constructor("ZodISODuration", (inst, def) => {
      $ZodISODuration.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  function duration(params) {
      return _isoDuration(ZodISODuration, params);
  }

  const initializer = (inst, issues) => {
      $ZodError.init(inst, issues);
      inst.name = "ZodError";
      Object.defineProperties(inst, {
          format: {
              value: (mapper) => formatError(inst, mapper),
              // enumerable: false,
          },
          flatten: {
              value: (mapper) => flattenError(inst, mapper),
              // enumerable: false,
          },
          addIssue: {
              value: (issue) => {
                  inst.issues.push(issue);
                  inst.message = JSON.stringify(inst.issues, jsonStringifyReplacer, 2);
              },
              // enumerable: false,
          },
          addIssues: {
              value: (issues) => {
                  inst.issues.push(...issues);
                  inst.message = JSON.stringify(inst.issues, jsonStringifyReplacer, 2);
              },
              // enumerable: false,
          },
          isEmpty: {
              get() {
                  return inst.issues.length === 0;
              },
              // enumerable: false,
          },
      });
      // Object.defineProperty(inst, "isEmpty", {
      //   get() {
      //     return inst.issues.length === 0;
      //   },
      // });
  };
  const ZodRealError = $constructor("ZodError", initializer, {
      Parent: Error,
  });
  // /** @deprecated Use `z.core.$ZodErrorMapCtx` instead. */
  // export type ErrorMapCtx = core.$ZodErrorMapCtx;

  const parse$1 = /* @__PURE__ */ _parse(ZodRealError);
  const parseAsync = /* @__PURE__ */ _parseAsync(ZodRealError);
  const safeParse = /* @__PURE__ */ _safeParse(ZodRealError);
  const safeParseAsync = /* @__PURE__ */ _safeParseAsync(ZodRealError);
  // Codec functions
  const encode = /* @__PURE__ */ _encode(ZodRealError);
  const decode = /* @__PURE__ */ _decode(ZodRealError);
  const encodeAsync = /* @__PURE__ */ _encodeAsync(ZodRealError);
  const decodeAsync = /* @__PURE__ */ _decodeAsync(ZodRealError);
  const safeEncode = /* @__PURE__ */ _safeEncode(ZodRealError);
  const safeDecode = /* @__PURE__ */ _safeDecode(ZodRealError);
  const safeEncodeAsync = /* @__PURE__ */ _safeEncodeAsync(ZodRealError);
  const safeDecodeAsync = /* @__PURE__ */ _safeDecodeAsync(ZodRealError);

  const ZodType = /*@__PURE__*/ $constructor("ZodType", (inst, def) => {
      $ZodType.init(inst, def);
      Object.assign(inst["~standard"], {
          jsonSchema: {
              input: createStandardJSONSchemaMethod(inst, "input"),
              output: createStandardJSONSchemaMethod(inst, "output"),
          },
      });
      inst.toJSONSchema = createToJSONSchemaMethod(inst, {});
      inst.def = def;
      inst.type = def.type;
      Object.defineProperty(inst, "_def", { value: def });
      // base methods
      inst.check = (...checks) => {
          return inst.clone(mergeDefs(def, {
              checks: [
                  ...(def.checks ?? []),
                  ...checks.map((ch) => typeof ch === "function" ? { _zod: { check: ch, def: { check: "custom" }, onattach: [] } } : ch),
              ],
          }));
      };
      inst.clone = (def, params) => clone(inst, def, params);
      inst.brand = () => inst;
      inst.register = ((reg, meta) => {
          reg.add(inst, meta);
          return inst;
      });
      // parsing
      inst.parse = (data, params) => parse$1(inst, data, params, { callee: inst.parse });
      inst.safeParse = (data, params) => safeParse(inst, data, params);
      inst.parseAsync = async (data, params) => parseAsync(inst, data, params, { callee: inst.parseAsync });
      inst.safeParseAsync = async (data, params) => safeParseAsync(inst, data, params);
      inst.spa = inst.safeParseAsync;
      // encoding/decoding
      inst.encode = (data, params) => encode(inst, data, params);
      inst.decode = (data, params) => decode(inst, data, params);
      inst.encodeAsync = async (data, params) => encodeAsync(inst, data, params);
      inst.decodeAsync = async (data, params) => decodeAsync(inst, data, params);
      inst.safeEncode = (data, params) => safeEncode(inst, data, params);
      inst.safeDecode = (data, params) => safeDecode(inst, data, params);
      inst.safeEncodeAsync = async (data, params) => safeEncodeAsync(inst, data, params);
      inst.safeDecodeAsync = async (data, params) => safeDecodeAsync(inst, data, params);
      // refinements
      inst.refine = (check, params) => inst.check(refine(check, params));
      inst.superRefine = (refinement) => inst.check(superRefine(refinement));
      inst.overwrite = (fn) => inst.check(_overwrite(fn));
      // wrappers
      inst.optional = () => optional(inst);
      inst.nullable = () => nullable(inst);
      inst.nullish = () => optional(nullable(inst));
      inst.nonoptional = (params) => nonoptional(inst, params);
      inst.array = () => array(inst);
      inst.or = (arg) => union([inst, arg]);
      inst.and = (arg) => intersection(inst, arg);
      inst.transform = (tx) => pipe(inst, transform(tx));
      inst.default = (def) => _default(inst, def);
      inst.prefault = (def) => prefault(inst, def);
      // inst.coalesce = (def, params) => coalesce(inst, def, params);
      inst.catch = (params) => _catch(inst, params);
      inst.pipe = (target) => pipe(inst, target);
      inst.readonly = () => readonly(inst);
      // meta
      inst.describe = (description) => {
          const cl = inst.clone();
          globalRegistry.add(cl, { description });
          return cl;
      };
      Object.defineProperty(inst, "description", {
          get() {
              return globalRegistry.get(inst)?.description;
          },
          configurable: true,
      });
      inst.meta = (...args) => {
          if (args.length === 0) {
              return globalRegistry.get(inst);
          }
          const cl = inst.clone();
          globalRegistry.add(cl, args[0]);
          return cl;
      };
      // helpers
      inst.isOptional = () => inst.safeParse(undefined).success;
      inst.isNullable = () => inst.safeParse(null).success;
      return inst;
  });
  /** @internal */
  const _ZodString = /*@__PURE__*/ $constructor("_ZodString", (inst, def) => {
      $ZodString.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => stringProcessor(inst, ctx, json);
      const bag = inst._zod.bag;
      inst.format = bag.format ?? null;
      inst.minLength = bag.minimum ?? null;
      inst.maxLength = bag.maximum ?? null;
      // validations
      inst.regex = (...args) => inst.check(_regex(...args));
      inst.includes = (...args) => inst.check(_includes(...args));
      inst.startsWith = (...args) => inst.check(_startsWith(...args));
      inst.endsWith = (...args) => inst.check(_endsWith(...args));
      inst.min = (...args) => inst.check(_minLength(...args));
      inst.max = (...args) => inst.check(_maxLength(...args));
      inst.length = (...args) => inst.check(_length(...args));
      inst.nonempty = (...args) => inst.check(_minLength(1, ...args));
      inst.lowercase = (params) => inst.check(_lowercase(params));
      inst.uppercase = (params) => inst.check(_uppercase(params));
      // transforms
      inst.trim = () => inst.check(_trim());
      inst.normalize = (...args) => inst.check(_normalize(...args));
      inst.toLowerCase = () => inst.check(_toLowerCase());
      inst.toUpperCase = () => inst.check(_toUpperCase());
      inst.slugify = () => inst.check(_slugify());
  });
  const ZodString = /*@__PURE__*/ $constructor("ZodString", (inst, def) => {
      $ZodString.init(inst, def);
      _ZodString.init(inst, def);
      inst.email = (params) => inst.check(_email(ZodEmail, params));
      inst.url = (params) => inst.check(_url(ZodURL, params));
      inst.jwt = (params) => inst.check(_jwt(ZodJWT, params));
      inst.emoji = (params) => inst.check(_emoji(ZodEmoji, params));
      inst.guid = (params) => inst.check(_guid(ZodGUID, params));
      inst.uuid = (params) => inst.check(_uuid(ZodUUID, params));
      inst.uuidv4 = (params) => inst.check(_uuidv4(ZodUUID, params));
      inst.uuidv6 = (params) => inst.check(_uuidv6(ZodUUID, params));
      inst.uuidv7 = (params) => inst.check(_uuidv7(ZodUUID, params));
      inst.nanoid = (params) => inst.check(_nanoid(ZodNanoID, params));
      inst.guid = (params) => inst.check(_guid(ZodGUID, params));
      inst.cuid = (params) => inst.check(_cuid(ZodCUID, params));
      inst.cuid2 = (params) => inst.check(_cuid2(ZodCUID2, params));
      inst.ulid = (params) => inst.check(_ulid(ZodULID, params));
      inst.base64 = (params) => inst.check(_base64(ZodBase64, params));
      inst.base64url = (params) => inst.check(_base64url(ZodBase64URL, params));
      inst.xid = (params) => inst.check(_xid(ZodXID, params));
      inst.ksuid = (params) => inst.check(_ksuid(ZodKSUID, params));
      inst.ipv4 = (params) => inst.check(_ipv4(ZodIPv4, params));
      inst.ipv6 = (params) => inst.check(_ipv6(ZodIPv6, params));
      inst.cidrv4 = (params) => inst.check(_cidrv4(ZodCIDRv4, params));
      inst.cidrv6 = (params) => inst.check(_cidrv6(ZodCIDRv6, params));
      inst.e164 = (params) => inst.check(_e164(ZodE164, params));
      // iso
      inst.datetime = (params) => inst.check(datetime(params));
      inst.date = (params) => inst.check(date(params));
      inst.time = (params) => inst.check(time(params));
      inst.duration = (params) => inst.check(duration(params));
  });
  function string(params) {
      return _string(ZodString, params);
  }
  const ZodStringFormat = /*@__PURE__*/ $constructor("ZodStringFormat", (inst, def) => {
      $ZodStringFormat.init(inst, def);
      _ZodString.init(inst, def);
  });
  const ZodEmail = /*@__PURE__*/ $constructor("ZodEmail", (inst, def) => {
      // ZodStringFormat.init(inst, def);
      $ZodEmail.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodGUID = /*@__PURE__*/ $constructor("ZodGUID", (inst, def) => {
      // ZodStringFormat.init(inst, def);
      $ZodGUID.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodUUID = /*@__PURE__*/ $constructor("ZodUUID", (inst, def) => {
      // ZodStringFormat.init(inst, def);
      $ZodUUID.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodURL = /*@__PURE__*/ $constructor("ZodURL", (inst, def) => {
      // ZodStringFormat.init(inst, def);
      $ZodURL.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodEmoji = /*@__PURE__*/ $constructor("ZodEmoji", (inst, def) => {
      // ZodStringFormat.init(inst, def);
      $ZodEmoji.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodNanoID = /*@__PURE__*/ $constructor("ZodNanoID", (inst, def) => {
      // ZodStringFormat.init(inst, def);
      $ZodNanoID.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodCUID = /*@__PURE__*/ $constructor("ZodCUID", (inst, def) => {
      // ZodStringFormat.init(inst, def);
      $ZodCUID.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodCUID2 = /*@__PURE__*/ $constructor("ZodCUID2", (inst, def) => {
      // ZodStringFormat.init(inst, def);
      $ZodCUID2.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodULID = /*@__PURE__*/ $constructor("ZodULID", (inst, def) => {
      // ZodStringFormat.init(inst, def);
      $ZodULID.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodXID = /*@__PURE__*/ $constructor("ZodXID", (inst, def) => {
      // ZodStringFormat.init(inst, def);
      $ZodXID.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodKSUID = /*@__PURE__*/ $constructor("ZodKSUID", (inst, def) => {
      // ZodStringFormat.init(inst, def);
      $ZodKSUID.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodIPv4 = /*@__PURE__*/ $constructor("ZodIPv4", (inst, def) => {
      // ZodStringFormat.init(inst, def);
      $ZodIPv4.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodIPv6 = /*@__PURE__*/ $constructor("ZodIPv6", (inst, def) => {
      // ZodStringFormat.init(inst, def);
      $ZodIPv6.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodCIDRv4 = /*@__PURE__*/ $constructor("ZodCIDRv4", (inst, def) => {
      $ZodCIDRv4.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodCIDRv6 = /*@__PURE__*/ $constructor("ZodCIDRv6", (inst, def) => {
      $ZodCIDRv6.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodBase64 = /*@__PURE__*/ $constructor("ZodBase64", (inst, def) => {
      // ZodStringFormat.init(inst, def);
      $ZodBase64.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodBase64URL = /*@__PURE__*/ $constructor("ZodBase64URL", (inst, def) => {
      // ZodStringFormat.init(inst, def);
      $ZodBase64URL.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodE164 = /*@__PURE__*/ $constructor("ZodE164", (inst, def) => {
      // ZodStringFormat.init(inst, def);
      $ZodE164.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodJWT = /*@__PURE__*/ $constructor("ZodJWT", (inst, def) => {
      // ZodStringFormat.init(inst, def);
      $ZodJWT.init(inst, def);
      ZodStringFormat.init(inst, def);
  });
  const ZodNumber = /*@__PURE__*/ $constructor("ZodNumber", (inst, def) => {
      $ZodNumber.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => numberProcessor(inst, ctx, json);
      inst.gt = (value, params) => inst.check(_gt(value, params));
      inst.gte = (value, params) => inst.check(_gte(value, params));
      inst.min = (value, params) => inst.check(_gte(value, params));
      inst.lt = (value, params) => inst.check(_lt(value, params));
      inst.lte = (value, params) => inst.check(_lte(value, params));
      inst.max = (value, params) => inst.check(_lte(value, params));
      inst.int = (params) => inst.check(int(params));
      inst.safe = (params) => inst.check(int(params));
      inst.positive = (params) => inst.check(_gt(0, params));
      inst.nonnegative = (params) => inst.check(_gte(0, params));
      inst.negative = (params) => inst.check(_lt(0, params));
      inst.nonpositive = (params) => inst.check(_lte(0, params));
      inst.multipleOf = (value, params) => inst.check(_multipleOf(value, params));
      inst.step = (value, params) => inst.check(_multipleOf(value, params));
      // inst.finite = (params) => inst.check(core.finite(params));
      inst.finite = () => inst;
      const bag = inst._zod.bag;
      inst.minValue =
          Math.max(bag.minimum ?? Number.NEGATIVE_INFINITY, bag.exclusiveMinimum ?? Number.NEGATIVE_INFINITY) ?? null;
      inst.maxValue =
          Math.min(bag.maximum ?? Number.POSITIVE_INFINITY, bag.exclusiveMaximum ?? Number.POSITIVE_INFINITY) ?? null;
      inst.isInt = (bag.format ?? "").includes("int") || Number.isSafeInteger(bag.multipleOf ?? 0.5);
      inst.isFinite = true;
      inst.format = bag.format ?? null;
  });
  function number(params) {
      return _number(ZodNumber, params);
  }
  const ZodNumberFormat = /*@__PURE__*/ $constructor("ZodNumberFormat", (inst, def) => {
      $ZodNumberFormat.init(inst, def);
      ZodNumber.init(inst, def);
  });
  function int(params) {
      return _int(ZodNumberFormat, params);
  }
  const ZodBoolean = /*@__PURE__*/ $constructor("ZodBoolean", (inst, def) => {
      $ZodBoolean.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => booleanProcessor(inst, ctx, json);
  });
  function boolean(params) {
      return _boolean(ZodBoolean, params);
  }
  const ZodAny = /*@__PURE__*/ $constructor("ZodAny", (inst, def) => {
      $ZodAny.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => anyProcessor();
  });
  function any() {
      return _any(ZodAny);
  }
  const ZodUnknown = /*@__PURE__*/ $constructor("ZodUnknown", (inst, def) => {
      $ZodUnknown.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => unknownProcessor();
  });
  function unknown() {
      return _unknown(ZodUnknown);
  }
  const ZodNever = /*@__PURE__*/ $constructor("ZodNever", (inst, def) => {
      $ZodNever.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => neverProcessor(inst, ctx, json);
  });
  function never(params) {
      return _never(ZodNever, params);
  }
  const ZodArray = /*@__PURE__*/ $constructor("ZodArray", (inst, def) => {
      $ZodArray.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => arrayProcessor(inst, ctx, json, params);
      inst.element = def.element;
      inst.min = (minLength, params) => inst.check(_minLength(minLength, params));
      inst.nonempty = (params) => inst.check(_minLength(1, params));
      inst.max = (maxLength, params) => inst.check(_maxLength(maxLength, params));
      inst.length = (len, params) => inst.check(_length(len, params));
      inst.unwrap = () => inst.element;
  });
  function array(element, params) {
      return _array(ZodArray, element, params);
  }
  const ZodObject = /*@__PURE__*/ $constructor("ZodObject", (inst, def) => {
      $ZodObjectJIT.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => objectProcessor(inst, ctx, json, params);
      defineLazy(inst, "shape", () => {
          return def.shape;
      });
      inst.keyof = () => _enum(Object.keys(inst._zod.def.shape));
      inst.catchall = (catchall) => inst.clone({ ...inst._zod.def, catchall: catchall });
      inst.passthrough = () => inst.clone({ ...inst._zod.def, catchall: unknown() });
      inst.loose = () => inst.clone({ ...inst._zod.def, catchall: unknown() });
      inst.strict = () => inst.clone({ ...inst._zod.def, catchall: never() });
      inst.strip = () => inst.clone({ ...inst._zod.def, catchall: undefined });
      inst.extend = (incoming) => {
          return extend(inst, incoming);
      };
      inst.safeExtend = (incoming) => {
          return safeExtend(inst, incoming);
      };
      inst.merge = (other) => merge(inst, other);
      inst.pick = (mask) => pick(inst, mask);
      inst.omit = (mask) => omit(inst, mask);
      inst.partial = (...args) => partial(ZodOptional, inst, args[0]);
      inst.required = (...args) => required(ZodNonOptional, inst, args[0]);
  });
  function object(shape, params) {
      const def = {
          type: "object",
          shape: shape ?? {},
          ...normalizeParams(params),
      };
      return new ZodObject(def);
  }
  const ZodUnion = /*@__PURE__*/ $constructor("ZodUnion", (inst, def) => {
      $ZodUnion.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => unionProcessor(inst, ctx, json, params);
      inst.options = def.options;
  });
  function union(options, params) {
      return new ZodUnion({
          type: "union",
          options: options,
          ...normalizeParams(params),
      });
  }
  const ZodIntersection = /*@__PURE__*/ $constructor("ZodIntersection", (inst, def) => {
      $ZodIntersection.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => intersectionProcessor(inst, ctx, json, params);
  });
  function intersection(left, right) {
      return new ZodIntersection({
          type: "intersection",
          left: left,
          right: right,
      });
  }
  const ZodTuple = /*@__PURE__*/ $constructor("ZodTuple", (inst, def) => {
      $ZodTuple.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => tupleProcessor(inst, ctx, json, params);
      inst.rest = (rest) => inst.clone({
          ...inst._zod.def,
          rest: rest,
      });
  });
  function tuple(items, _paramsOrRest, _params) {
      const hasRest = _paramsOrRest instanceof $ZodType;
      const params = hasRest ? _params : _paramsOrRest;
      const rest = hasRest ? _paramsOrRest : null;
      return new ZodTuple({
          type: "tuple",
          items: items,
          rest,
          ...normalizeParams(params),
      });
  }
  const ZodEnum = /*@__PURE__*/ $constructor("ZodEnum", (inst, def) => {
      $ZodEnum.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => enumProcessor(inst, ctx, json);
      inst.enum = def.entries;
      inst.options = Object.values(def.entries);
      const keys = new Set(Object.keys(def.entries));
      inst.extract = (values, params) => {
          const newEntries = {};
          for (const value of values) {
              if (keys.has(value)) {
                  newEntries[value] = def.entries[value];
              }
              else
                  throw new Error(`Key ${value} not found in enum`);
          }
          return new ZodEnum({
              ...def,
              checks: [],
              ...normalizeParams(params),
              entries: newEntries,
          });
      };
      inst.exclude = (values, params) => {
          const newEntries = { ...def.entries };
          for (const value of values) {
              if (keys.has(value)) {
                  delete newEntries[value];
              }
              else
                  throw new Error(`Key ${value} not found in enum`);
          }
          return new ZodEnum({
              ...def,
              checks: [],
              ...normalizeParams(params),
              entries: newEntries,
          });
      };
  });
  function _enum(values, params) {
      const entries = Array.isArray(values) ? Object.fromEntries(values.map((v) => [v, v])) : values;
      return new ZodEnum({
          type: "enum",
          entries,
          ...normalizeParams(params),
      });
  }
  const ZodLiteral = /*@__PURE__*/ $constructor("ZodLiteral", (inst, def) => {
      $ZodLiteral.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => literalProcessor(inst, ctx, json);
      inst.values = new Set(def.values);
      Object.defineProperty(inst, "value", {
          get() {
              if (def.values.length > 1) {
                  throw new Error("This schema contains multiple valid literal values. Use `.values` instead.");
              }
              return def.values[0];
          },
      });
  });
  function literal$1(value, params) {
      return new ZodLiteral({
          type: "literal",
          values: Array.isArray(value) ? value : [value],
          ...normalizeParams(params),
      });
  }
  const ZodTransform = /*@__PURE__*/ $constructor("ZodTransform", (inst, def) => {
      $ZodTransform.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => transformProcessor(inst, ctx);
      inst._zod.parse = (payload, _ctx) => {
          if (_ctx.direction === "backward") {
              throw new $ZodEncodeError(inst.constructor.name);
          }
          payload.addIssue = (issue$1) => {
              if (typeof issue$1 === "string") {
                  payload.issues.push(issue(issue$1, payload.value, def));
              }
              else {
                  // for Zod 3 backwards compatibility
                  const _issue = issue$1;
                  if (_issue.fatal)
                      _issue.continue = false;
                  _issue.code ?? (_issue.code = "custom");
                  _issue.input ?? (_issue.input = payload.value);
                  _issue.inst ?? (_issue.inst = inst);
                  // _issue.continue ??= true;
                  payload.issues.push(issue(_issue));
              }
          };
          const output = def.transform(payload.value, payload);
          if (output instanceof Promise) {
              return output.then((output) => {
                  payload.value = output;
                  return payload;
              });
          }
          payload.value = output;
          return payload;
      };
  });
  function transform(fn) {
      return new ZodTransform({
          type: "transform",
          transform: fn,
      });
  }
  const ZodOptional = /*@__PURE__*/ $constructor("ZodOptional", (inst, def) => {
      $ZodOptional.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => optionalProcessor(inst, ctx, json, params);
      inst.unwrap = () => inst._zod.def.innerType;
  });
  function optional(innerType) {
      return new ZodOptional({
          type: "optional",
          innerType: innerType,
      });
  }
  const ZodNullable = /*@__PURE__*/ $constructor("ZodNullable", (inst, def) => {
      $ZodNullable.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => nullableProcessor(inst, ctx, json, params);
      inst.unwrap = () => inst._zod.def.innerType;
  });
  function nullable(innerType) {
      return new ZodNullable({
          type: "nullable",
          innerType: innerType,
      });
  }
  const ZodDefault = /*@__PURE__*/ $constructor("ZodDefault", (inst, def) => {
      $ZodDefault.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => defaultProcessor(inst, ctx, json, params);
      inst.unwrap = () => inst._zod.def.innerType;
      inst.removeDefault = inst.unwrap;
  });
  function _default(innerType, defaultValue) {
      return new ZodDefault({
          type: "default",
          innerType: innerType,
          get defaultValue() {
              return typeof defaultValue === "function" ? defaultValue() : shallowClone(defaultValue);
          },
      });
  }
  const ZodPrefault = /*@__PURE__*/ $constructor("ZodPrefault", (inst, def) => {
      $ZodPrefault.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => prefaultProcessor(inst, ctx, json, params);
      inst.unwrap = () => inst._zod.def.innerType;
  });
  function prefault(innerType, defaultValue) {
      return new ZodPrefault({
          type: "prefault",
          innerType: innerType,
          get defaultValue() {
              return typeof defaultValue === "function" ? defaultValue() : shallowClone(defaultValue);
          },
      });
  }
  const ZodNonOptional = /*@__PURE__*/ $constructor("ZodNonOptional", (inst, def) => {
      $ZodNonOptional.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => nonoptionalProcessor(inst, ctx, json, params);
      inst.unwrap = () => inst._zod.def.innerType;
  });
  function nonoptional(innerType, params) {
      return new ZodNonOptional({
          type: "nonoptional",
          innerType: innerType,
          ...normalizeParams(params),
      });
  }
  const ZodCatch = /*@__PURE__*/ $constructor("ZodCatch", (inst, def) => {
      $ZodCatch.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => catchProcessor(inst, ctx, json, params);
      inst.unwrap = () => inst._zod.def.innerType;
      inst.removeCatch = inst.unwrap;
  });
  function _catch(innerType, catchValue) {
      return new ZodCatch({
          type: "catch",
          innerType: innerType,
          catchValue: (typeof catchValue === "function" ? catchValue : () => catchValue),
      });
  }
  const ZodPipe = /*@__PURE__*/ $constructor("ZodPipe", (inst, def) => {
      $ZodPipe.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => pipeProcessor(inst, ctx, json, params);
      inst.in = def.in;
      inst.out = def.out;
  });
  function pipe(in_, out) {
      return new ZodPipe({
          type: "pipe",
          in: in_,
          out: out,
          // ...util.normalizeParams(params),
      });
  }
  const ZodReadonly = /*@__PURE__*/ $constructor("ZodReadonly", (inst, def) => {
      $ZodReadonly.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => readonlyProcessor(inst, ctx, json, params);
      inst.unwrap = () => inst._zod.def.innerType;
  });
  function readonly(innerType) {
      return new ZodReadonly({
          type: "readonly",
          innerType: innerType,
      });
  }
  const ZodFunction = /*@__PURE__*/ $constructor("ZodFunction", (inst, def) => {
      $ZodFunction.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => functionProcessor(inst, ctx);
  });
  function _function(params) {
      return new ZodFunction({
          type: "function",
          input: Array.isArray(params?.input) ? tuple(params?.input) : (array(unknown())),
          output: unknown(),
      });
  }
  const ZodCustom = /*@__PURE__*/ $constructor("ZodCustom", (inst, def) => {
      $ZodCustom.init(inst, def);
      ZodType.init(inst, def);
      inst._zod.processJSONSchema = (ctx, json, params) => customProcessor(inst, ctx);
  });
  function custom(fn, _params) {
      return _custom(ZodCustom, fn ?? (() => true), _params);
  }
  function refine(fn, _params = {}) {
      return _refine(ZodCustom, fn, _params);
  }
  // superRefine
  function superRefine(fn) {
      return _superRefine(fn);
  }
  function _instanceof(cls, params = {
      error: `Input not instance of ${cls.name}`,
  }) {
      const inst = new ZodCustom({
          type: "custom",
          check: "custom",
          fn: (data) => data instanceof cls,
          abort: true,
          ...normalizeParams(params),
      });
      inst._zod.bag.Class = cls;
      return inst;
  }

  var p5$1 = {
  	describe: {
  		overloads: [
  			[
  				"String",
  				"FALLBACK|LABEL?"
  			]
  		]
  	},
  	describeElement: {
  		overloads: [
  			[
  				"String",
  				"String",
  				"FALLBACK|LABEL?"
  			]
  		]
  	},
  	textOutput: {
  		overloads: [
  			[
  				"FALLBACK|LABEL?"
  			]
  		]
  	},
  	gridOutput: {
  		overloads: [
  			[
  				"FALLBACK|LABEL?"
  			]
  		]
  	},
  	remove: {
  		overloads: [
  			[
  			]
  		]
  	},
  	p5: {
  		overloads: [
  			[
  				"Object",
  				"String|HTMLElement"
  			]
  		]
  	},
  	color: {
  		overloads: [
  			[
  				"Number",
  				"Number?"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number?"
  			],
  			[
  				"String"
  			],
  			[
  				"Number[]"
  			],
  			[
  				"p5.Color"
  			]
  		]
  	},
  	red: {
  		overloads: [
  			[
  				"p5.Color|Number[]|String"
  			]
  		]
  	},
  	green: {
  		overloads: [
  			[
  				"p5.Color|Number[]|String"
  			]
  		]
  	},
  	blue: {
  		overloads: [
  			[
  				"p5.Color|Number[]|String"
  			]
  		]
  	},
  	alpha: {
  		overloads: [
  			[
  				"p5.Color|Number[]|String"
  			]
  		]
  	},
  	hue: {
  		overloads: [
  			[
  				"p5.Color|Number[]|String"
  			]
  		]
  	},
  	saturation: {
  		overloads: [
  			[
  				"p5.Color|Number[]|String"
  			]
  		]
  	},
  	brightness: {
  		overloads: [
  			[
  				"p5.Color|Number[]|String"
  			]
  		]
  	},
  	lightness: {
  		overloads: [
  			[
  				"p5.Color|Number[]|String"
  			]
  		]
  	},
  	lerpColor: {
  		overloads: [
  			[
  				"p5.Color",
  				"p5.Color",
  				"Number"
  			]
  		]
  	},
  	paletteLerp: {
  		overloads: [
  			[
  				"[p5.Color|String|Number|Number[], Number][]",
  				"Number"
  			]
  		]
  	},
  	beginClip: {
  		overloads: [
  			[
  				"Object?"
  			]
  		]
  	},
  	endClip: {
  		overloads: [
  			[
  			]
  		]
  	},
  	clip: {
  		overloads: [
  			[
  				"Function",
  				"Object?"
  			]
  		]
  	},
  	background: {
  		overloads: [
  			[
  				"p5.Color"
  			],
  			[
  				"String",
  				"Number?"
  			],
  			[
  				"Number",
  				"Number?"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number?"
  			],
  			[
  				"Number[]"
  			],
  			[
  				"p5.Image",
  				"Number?"
  			]
  		]
  	},
  	clear: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?"
  			],
  			[
  			]
  		]
  	},
  	colorMode: {
  		overloads: [
  			[
  				"RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH",
  				"Number?"
  			],
  			[
  				"RGB|HSB|HSL|RGBHDR|HWB|LAB|LCH|OKLAB|OKLCH",
  				"Number",
  				"Number",
  				"Number",
  				"Number?"
  			],
  			[
  			]
  		]
  	},
  	fill: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number?"
  			],
  			[
  				"String"
  			],
  			[
  				"Number",
  				"Number?"
  			],
  			[
  				"Number[]"
  			],
  			[
  				"p5.Color"
  			]
  		]
  	},
  	noFill: {
  		overloads: [
  			[
  			]
  		]
  	},
  	noStroke: {
  		overloads: [
  			[
  			]
  		]
  	},
  	stroke: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number?"
  			],
  			[
  				"String"
  			],
  			[
  				"Number",
  				"Number?"
  			],
  			[
  				"Number[]"
  			],
  			[
  				"p5.Color"
  			]
  		]
  	},
  	erase: {
  		overloads: [
  			[
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	noErase: {
  		overloads: [
  			[
  			]
  		]
  	},
  	blendMode: {
  		overloads: [
  			[
  				"BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|REMOVE|SUBTRACT"
  			]
  		]
  	},
  	print: {
  		overloads: [
  			[
  				"Any"
  			],
  			[
  				"String|Number|Array"
  			]
  		]
  	},
  	cursor: {
  		overloads: [
  			[
  				"ARROW|CROSS|HAND|MOVE|TEXT|WAIT|String",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	frameRate: {
  		overloads: [
  			[
  				"Number"
  			],
  			[
  			]
  		]
  	},
  	getTargetFrameRate: {
  		overloads: [
  			[
  			]
  		]
  	},
  	noCursor: {
  		overloads: [
  			[
  			]
  		]
  	},
  	windowResized: {
  		overloads: [
  			[
  				"Event?"
  			]
  		]
  	},
  	fullscreen: {
  		overloads: [
  			[
  				"Boolean?"
  			]
  		]
  	},
  	pixelDensity: {
  		overloads: [
  			[
  				"Number?"
  			],
  			[
  			]
  		]
  	},
  	displayDensity: {
  		overloads: [
  			[
  			]
  		]
  	},
  	getURL: {
  		overloads: [
  			[
  			]
  		]
  	},
  	getURLPath: {
  		overloads: [
  			[
  			]
  		]
  	},
  	getURLParams: {
  		overloads: [
  			[
  			]
  		]
  	},
  	worldToScreen: {
  		overloads: [
  			[
  				"Number|p5.Vector",
  				"Number",
  				"Number?"
  			]
  		]
  	},
  	screenToWorld: {
  		overloads: [
  			[
  				"Number|p5.Vector",
  				"Number",
  				"Number?"
  			]
  		]
  	},
  	setup: {
  		overloads: [
  			[
  			]
  		]
  	},
  	draw: {
  		overloads: [
  			[
  			]
  		]
  	},
  	registerAddon: {
  		overloads: [
  			[
  				"Function"
  			]
  		]
  	},
  	createCanvas: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"P2D|WEBGL|P2DHDR|WEBGPU?",
  				"HTMLCanvasElement?"
  			],
  			[
  				"Number?",
  				"Number?",
  				"HTMLCanvasElement?"
  			]
  		]
  	},
  	resizeCanvas: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Boolean?"
  			]
  		]
  	},
  	noCanvas: {
  		overloads: [
  			[
  			]
  		]
  	},
  	createGraphics: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"P2D|WEBGL?",
  				"HTMLCanvasElement?"
  			],
  			[
  				"Number",
  				"Number",
  				"HTMLCanvasElement?"
  			]
  		]
  	},
  	createFramebuffer: {
  		overloads: [
  			[
  				"Object?"
  			]
  		]
  	},
  	clearDepth: {
  		overloads: [
  			[
  				"Number?"
  			]
  		]
  	},
  	noLoop: {
  		overloads: [
  			[
  			]
  		]
  	},
  	loop: {
  		overloads: [
  			[
  			]
  		]
  	},
  	isLooping: {
  		overloads: [
  			[
  			]
  		]
  	},
  	redraw: {
  		overloads: [
  			[
  				"Integer?"
  			]
  		]
  	},
  	applyMatrix: {
  		overloads: [
  			[
  				"Number[]"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	resetMatrix: {
  		overloads: [
  			[
  			]
  		]
  	},
  	rotate: {
  		overloads: [
  			[
  				"Number",
  				"p5.Vector|Number[]?"
  			]
  		]
  	},
  	rotateX: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	rotateY: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	rotateZ: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	scale: {
  		overloads: [
  			[
  				"Number|p5.Vector|Number[]",
  				"Number?",
  				"Number?"
  			],
  			[
  				"p5.Vector|Number[]"
  			]
  		]
  	},
  	shearX: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	shearY: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	translate: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number?"
  			],
  			[
  				"p5.Vector"
  			]
  		]
  	},
  	push: {
  		overloads: [
  			[
  			]
  		]
  	},
  	pop: {
  		overloads: [
  			[
  			]
  		]
  	},
  	storeItem: {
  		overloads: [
  			[
  				"String",
  				"String|Number|Boolean|Object|Array"
  			]
  		]
  	},
  	getItem: {
  		overloads: [
  			[
  				"String"
  			]
  		]
  	},
  	clearStorage: {
  		overloads: [
  			[
  			]
  		]
  	},
  	removeItem: {
  		overloads: [
  			[
  				"String"
  			]
  		]
  	},
  	select: {
  		overloads: [
  			[
  				"String",
  				"String|p5.Element|HTMLElement?"
  			]
  		]
  	},
  	selectAll: {
  		overloads: [
  			[
  				"String",
  				"String|p5.Element|HTMLElement?"
  			]
  		]
  	},
  	createElement: {
  		overloads: [
  			[
  				"String",
  				"String?"
  			]
  		]
  	},
  	removeElements: {
  		overloads: [
  			[
  			]
  		]
  	},
  	addElement: {
  		overloads: [
  			[
  			]
  		]
  	},
  	createDiv: {
  		overloads: [
  			[
  				"String?"
  			]
  		]
  	},
  	createP: {
  		overloads: [
  			[
  				"String?"
  			]
  		]
  	},
  	createSpan: {
  		overloads: [
  			[
  				"String?"
  			]
  		]
  	},
  	createImg: {
  		overloads: [
  			[
  				"String",
  				"String"
  			],
  			[
  				"String",
  				"String",
  				"String?",
  				"Function?"
  			]
  		]
  	},
  	createA: {
  		overloads: [
  			[
  				"String",
  				"String",
  				"String?"
  			]
  		]
  	},
  	createSlider: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	createButton: {
  		overloads: [
  			[
  				"String",
  				"String?"
  			]
  		]
  	},
  	createCheckbox: {
  		overloads: [
  			[
  				"String?",
  				"Boolean?"
  			]
  		]
  	},
  	createSelect: {
  		overloads: [
  			[
  				"Boolean?"
  			],
  			[
  				"Object"
  			]
  		]
  	},
  	createRadio: {
  		overloads: [
  			[
  				"Object?"
  			],
  			[
  				"String?"
  			],
  			[
  			]
  		]
  	},
  	createColorPicker: {
  		overloads: [
  			[
  				"String|p5.Color?"
  			]
  		]
  	},
  	createInput: {
  		overloads: [
  			[
  				"String?",
  				"String?"
  			],
  			[
  				"String?"
  			]
  		]
  	},
  	createFileInput: {
  		overloads: [
  			[
  				"Function",
  				"Boolean?"
  			]
  		]
  	},
  	createVideo: {
  		overloads: [
  			[
  				"String|String[]?",
  				"Function?"
  			]
  		]
  	},
  	createAudio: {
  		overloads: [
  			[
  				"String|String[]?",
  				"Function?"
  			]
  		]
  	},
  	createCapture: {
  		overloads: [
  			[
  				"AUDIO|VIDEO|Object?",
  				"Object?",
  				"Function?"
  			]
  		]
  	},
  	setMoveThreshold: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	setShakeThreshold: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	deviceMoved: {
  		overloads: [
  			[
  			]
  		]
  	},
  	deviceTurned: {
  		overloads: [
  			[
  			]
  		]
  	},
  	deviceShaken: {
  		overloads: [
  			[
  			]
  		]
  	},
  	keyPressed: {
  		overloads: [
  			[
  				"KeyboardEvent?"
  			]
  		]
  	},
  	keyReleased: {
  		overloads: [
  			[
  				"KeyboardEvent?"
  			]
  		]
  	},
  	keyTyped: {
  		overloads: [
  			[
  				"KeyboardEvent?"
  			]
  		]
  	},
  	keyIsDown: {
  		overloads: [
  			[
  				"Number|String"
  			]
  		]
  	},
  	mouseMoved: {
  		overloads: [
  			[
  				"MouseEvent?"
  			]
  		]
  	},
  	mouseDragged: {
  		overloads: [
  			[
  				"MouseEvent?"
  			]
  		]
  	},
  	mousePressed: {
  		overloads: [
  			[
  				"MouseEvent?"
  			]
  		]
  	},
  	mouseReleased: {
  		overloads: [
  			[
  				"MouseEvent?"
  			]
  		]
  	},
  	mouseClicked: {
  		overloads: [
  			[
  				"MouseEvent?"
  			]
  		]
  	},
  	doubleClicked: {
  		overloads: [
  			[
  				"MouseEvent?"
  			]
  		]
  	},
  	mouseWheel: {
  		overloads: [
  			[
  				"WheelEvent?"
  			]
  		]
  	},
  	requestPointerLock: {
  		overloads: [
  			[
  			]
  		]
  	},
  	exitPointerLock: {
  		overloads: [
  			[
  			]
  		]
  	},
  	createImage: {
  		overloads: [
  			[
  				"Integer",
  				"Integer"
  			]
  		]
  	},
  	saveCanvas: {
  		overloads: [
  			[
  				"p5.Framebuffer|p5.Element|HTMLCanvasElement",
  				"String?",
  				"String?"
  			],
  			[
  				"String?",
  				"String?"
  			]
  		]
  	},
  	saveFrames: {
  		overloads: [
  			[
  				"String",
  				"String",
  				"Number",
  				"Number",
  				"function(Array)?"
  			]
  		]
  	},
  	loadImage: {
  		overloads: [
  			[
  				"String|Request",
  				"function(p5.Image)?",
  				"function(Event)?"
  			]
  		]
  	},
  	saveGif: {
  		overloads: [
  			[
  				"String",
  				"Number",
  				"Object?"
  			]
  		]
  	},
  	image: {
  		overloads: [
  			[
  				"p5.Image|p5.Element|p5.Texture|p5.Framebuffer|p5.FramebufferTexture|p5.Renderer|p5.Graphics",
  				"Number",
  				"Number",
  				"Number?",
  				"Number?"
  			],
  			[
  				"p5.Image|p5.Element|p5.Texture|p5.Framebuffer|p5.FramebufferTexture",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number?",
  				"Number?",
  				"CONTAIN|COVER?",
  				"LEFT|RIGHT|CENTER?",
  				"TOP|BOTTOM|CENTER?"
  			]
  		]
  	},
  	tint: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number?"
  			],
  			[
  				"String"
  			],
  			[
  				"Number",
  				"Number?"
  			],
  			[
  				"Number[]"
  			],
  			[
  				"p5.Color"
  			]
  		]
  	},
  	noTint: {
  		overloads: [
  			[
  			]
  		]
  	},
  	imageMode: {
  		overloads: [
  			[
  				"CORNER|CORNERS|CENTER"
  			]
  		]
  	},
  	blend: {
  		overloads: [
  			[
  				"p5.Image",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL"
  			],
  			[
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL"
  			]
  		]
  	},
  	copy: {
  		overloads: [
  			[
  				"p5.Image|p5.Element",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer"
  			],
  			[
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer"
  			]
  		]
  	},
  	filter: {
  		overloads: [
  			[
  				"THRESHOLD|GRAY|OPAQUE|INVERT|POSTERIZE|BLUR|ERODE|DILATE|BLUR",
  				"Number?",
  				"Boolean?"
  			],
  			[
  				"p5.Shader"
  			]
  		]
  	},
  	get: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			],
  			[
  			],
  			[
  				"Number",
  				"Number"
  			]
  		]
  	},
  	loadPixels: {
  		overloads: [
  			[
  			]
  		]
  	},
  	set: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number|Number[]|Object"
  			]
  		]
  	},
  	updatePixels: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?"
  			],
  			[
  			]
  		]
  	},
  	loadJSON: {
  		overloads: [
  			[
  				"String|Request",
  				"Function?",
  				"Function?"
  			]
  		]
  	},
  	loadStrings: {
  		overloads: [
  			[
  				"String|Request",
  				"Function?",
  				"Function?"
  			]
  		]
  	},
  	loadTable: {
  		overloads: [
  			[
  				"String|Request",
  				"String?",
  				"String?",
  				"Function?",
  				"Function?"
  			]
  		]
  	},
  	loadXML: {
  		overloads: [
  			[
  				"String|Request",
  				"Function?",
  				"Function?"
  			]
  		]
  	},
  	loadBytes: {
  		overloads: [
  			[
  				"String|Request",
  				"Function?",
  				"Function?"
  			]
  		]
  	},
  	loadBlob: {
  		overloads: [
  			[
  				"String|Request",
  				"Function?",
  				"Function?"
  			]
  		]
  	},
  	httpGet: {
  		overloads: [
  			[
  				"String|Request",
  				"String?",
  				"Function?",
  				"Function?"
  			],
  			[
  				"String|Request",
  				"Function",
  				"Function?"
  			]
  		]
  	},
  	httpPost: {
  		overloads: [
  			[
  				"String|Request",
  				"Object|Boolean?",
  				"String?",
  				"Function?",
  				"Function?"
  			],
  			[
  				"String|Request",
  				"Object|Boolean",
  				"Function?",
  				"Function?"
  			],
  			[
  				"String|Request",
  				"Function?",
  				"Function?"
  			]
  		]
  	},
  	httpDo: {
  		overloads: [
  			[
  				"String|Request",
  				"String?",
  				"String?",
  				"Object?",
  				"Function?",
  				"Function?"
  			],
  			[
  				"String|Request",
  				"Function?",
  				"Function?"
  			]
  		]
  	},
  	createWriter: {
  		overloads: [
  			[
  				"String",
  				"String?"
  			]
  		]
  	},
  	write: {
  		overloads: [
  			[
  				"String|Number|Array"
  			]
  		]
  	},
  	close: {
  		overloads: [
  			[
  			]
  		]
  	},
  	save: {
  		overloads: [
  			[
  				"Object|String?",
  				"String?",
  				"Boolean|String?"
  			]
  		]
  	},
  	saveJSON: {
  		overloads: [
  			[
  				"Array|Object",
  				"String",
  				"Boolean?"
  			]
  		]
  	},
  	saveStrings: {
  		overloads: [
  			[
  				"String[]",
  				"String",
  				"String?",
  				"Boolean?"
  			]
  		]
  	},
  	saveTable: {
  		overloads: [
  			[
  				"p5.Table",
  				"String",
  				"String?"
  			]
  		]
  	},
  	setContent: {
  		overloads: [
  			[
  				"String"
  			]
  		]
  	},
  	abs: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	ceil: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	constrain: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	dist: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	exp: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	floor: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	lerp: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	log: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	mag: {
  		overloads: [
  			[
  				"Number",
  				"Number"
  			]
  		]
  	},
  	map: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Boolean?"
  			]
  		]
  	},
  	max: {
  		overloads: [
  			[
  				"Number",
  				"Number"
  			],
  			[
  				"Number[]"
  			]
  		]
  	},
  	min: {
  		overloads: [
  			[
  				"Number",
  				"Number"
  			],
  			[
  				"Number[]"
  			]
  		]
  	},
  	norm: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	pow: {
  		overloads: [
  			[
  				"Number",
  				"Number"
  			]
  		]
  	},
  	round: {
  		overloads: [
  			[
  				"Number",
  				"Number?"
  			]
  		]
  	},
  	sq: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	sqrt: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	fract: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	createVector: {
  		overloads: [
  			[
  				"...Number[]"
  			]
  		]
  	},
  	noise: {
  		overloads: [
  			[
  				"Number",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	noiseDetail: {
  		overloads: [
  			[
  				"Number"
  			],
  			[
  				"Number",
  				"Number"
  			]
  		]
  	},
  	noiseSeed: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	randomSeed: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	random: {
  		overloads: [
  			[
  				"Number?",
  				"Number?"
  			],
  			[
  				"Array"
  			]
  		]
  	},
  	randomGaussian: {
  		overloads: [
  			[
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	acos: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	asin: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	atan: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	atan2: {
  		overloads: [
  			[
  				"Number",
  				"Number"
  			]
  		]
  	},
  	cos: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	sin: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	tan: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	degrees: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	radians: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	angleMode: {
  		overloads: [
  			[
  				"RADIANS|DEGREES"
  			],
  			[
  			]
  		]
  	},
  	arc: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"CHORD|PIE|OPEN?",
  				"Integer?"
  			]
  		]
  	},
  	ellipse: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number?"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Integer?"
  			]
  		]
  	},
  	circle: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	line: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	point: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number?"
  			],
  			[
  				"p5.Vector"
  			]
  		]
  	},
  	quad: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Integer?",
  				"Integer?"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Integer?",
  				"Integer?"
  			]
  		]
  	},
  	rect: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Integer?",
  				"Integer?"
  			]
  		]
  	},
  	square: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	triangle: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	ellipseMode: {
  		overloads: [
  			[
  				"CENTER|RADIUS|CORNER|CORNERS"
  			]
  		]
  	},
  	noSmooth: {
  		overloads: [
  			[
  			]
  		]
  	},
  	rectMode: {
  		overloads: [
  			[
  				"CENTER|RADIUS|CORNER|CORNERS"
  			]
  		]
  	},
  	smooth: {
  		overloads: [
  			[
  			]
  		]
  	},
  	strokeCap: {
  		overloads: [
  			[
  				"ROUND|SQUARE|PROJECT"
  			]
  		]
  	},
  	strokeJoin: {
  		overloads: [
  			[
  				"MITER|BEVEL|ROUND"
  			]
  		]
  	},
  	strokeWeight: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	bezier: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	bezierPoint: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	bezierTangent: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	spline: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	splinePoint: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	splineTangent: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	bezierOrder: {
  		overloads: [
  			[
  				"Number"
  			],
  			[
  			]
  		]
  	},
  	splineVertex: {
  		overloads: [
  			[
  				"Number",
  				"Number"
  			],
  			[
  				"Number",
  				"Number",
  				"Number?"
  			],
  			[
  				"Number",
  				"Number",
  				"Number?",
  				"Number?"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	splineProperty: {
  		overloads: [
  			[
  				"String",
  				null
  			],
  			[
  				"String"
  			]
  		]
  	},
  	splineProperties: {
  		overloads: [
  			[
  				"Object"
  			]
  		]
  	},
  	vertex: {
  		overloads: [
  			[
  				"Number",
  				"Number"
  			],
  			[
  				"Number",
  				"Number",
  				"Number?",
  				"Number?"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	beginContour: {
  		overloads: [
  			[
  			]
  		]
  	},
  	endContour: {
  		overloads: [
  			[
  				"OPEN|CLOSE?"
  			]
  		]
  	},
  	beginShape: {
  		overloads: [
  			[
  				"POINTS|LINES|TRIANGLES|TRIANGLE_FAN|TRIANGLE_STRIP|QUADS|QUAD_STRIP|PATH?"
  			]
  		]
  	},
  	bezierVertex: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number?",
  				"Number?"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	endShape: {
  		overloads: [
  			[
  				"CLOSE?",
  				"Integer?"
  			]
  		]
  	},
  	normal: {
  		overloads: [
  			[
  				"p5.Vector"
  			],
  			[
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	vertexProperty: {
  		overloads: [
  			[
  				"String",
  				"Number|Number[]"
  			]
  		]
  	},
  	getWorldInputs: {
  		overloads: [
  			[
  				"Function"
  			]
  		]
  	},
  	getPixelInputs: {
  		overloads: [
  			[
  				"Function"
  			]
  		]
  	},
  	getFinalColor: {
  		overloads: [
  			[
  				"Function"
  			]
  		]
  	},
  	getColor: {
  		overloads: [
  			[
  				"Function"
  			]
  		]
  	},
  	getObjectInputs: {
  		overloads: [
  			[
  				"Function"
  			]
  		]
  	},
  	getCameraInputs: {
  		overloads: [
  			[
  				"Function"
  			]
  		]
  	},
  	loadFont: {
  		overloads: [
  			[
  				"String",
  				"String?",
  				"Object?",
  				"Function?",
  				"Function?"
  			],
  			[
  				"String",
  				"Function?",
  				"Function?"
  			]
  		]
  	},
  	text: {
  		overloads: [
  			[
  				"String|Object|Array|Number|Boolean",
  				"Number",
  				"Number",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	textAlign: {
  		overloads: [
  			[
  				"LEFT|CENTER|RIGHT?",
  				"TOP|BOTTOM|CENTER|BASELINE?"
  			]
  		]
  	},
  	textAscent: {
  		overloads: [
  			[
  				"String?"
  			]
  		]
  	},
  	textDescent: {
  		overloads: [
  			[
  				"String?"
  			]
  		]
  	},
  	textLeading: {
  		overloads: [
  			[
  				"Number?"
  			]
  		]
  	},
  	textFont: {
  		overloads: [
  			[
  				"p5.Font|String|Object?",
  				"Number?"
  			]
  		]
  	},
  	textSize: {
  		overloads: [
  			[
  				"Number"
  			],
  			[
  			]
  		]
  	},
  	textStyle: {
  		overloads: [
  			[
  				"NORMAL|ITALIC|BOLD|BOLDITALIC"
  			],
  			[
  			]
  		]
  	},
  	textWidth: {
  		overloads: [
  			[
  				"String"
  			]
  		]
  	},
  	textWrap: {
  		overloads: [
  			[
  				"WORD|CHAR"
  			],
  			[
  			]
  		]
  	},
  	textBounds: {
  		overloads: [
  			[
  				"String",
  				"Number",
  				"Number",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	textDirection: {
  		overloads: [
  			[
  				"String"
  			],
  			[
  			]
  		]
  	},
  	textProperty: {
  		overloads: [
  			[
  				"String",
  				null
  			],
  			[
  				"String"
  			]
  		]
  	},
  	textProperties: {
  		overloads: [
  			[
  				"Object"
  			],
  			[
  			]
  		]
  	},
  	fontBounds: {
  		overloads: [
  			[
  				"String",
  				"Number",
  				"Number",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	fontWidth: {
  		overloads: [
  			[
  				"String"
  			]
  		]
  	},
  	fontAscent: {
  		overloads: [
  			[
  			]
  		]
  	},
  	fontDescent: {
  		overloads: [
  			[
  			]
  		]
  	},
  	textWeight: {
  		overloads: [
  			[
  				"Number"
  			],
  			[
  			]
  		]
  	},
  	float: {
  		overloads: [
  			[
  				"String"
  			],
  			[
  				"String[]"
  			]
  		]
  	},
  	int: {
  		overloads: [
  			[
  				"String|Boolean|Number"
  			],
  			[
  				"Array"
  			]
  		]
  	},
  	str: {
  		overloads: [
  			[
  				"String|Boolean|Number"
  			]
  		]
  	},
  	boolean: {
  		overloads: [
  			[
  				"String|Boolean|Number"
  			],
  			[
  				"Array"
  			]
  		]
  	},
  	byte: {
  		overloads: [
  			[
  				"String|Boolean|Number"
  			],
  			[
  				"Array"
  			]
  		]
  	},
  	char: {
  		overloads: [
  			[
  				"String|Number"
  			],
  			[
  				"Array"
  			]
  		]
  	},
  	unchar: {
  		overloads: [
  			[
  				"String"
  			],
  			[
  				"String[]"
  			]
  		]
  	},
  	hex: {
  		overloads: [
  			[
  				"Number",
  				"Number?"
  			],
  			[
  				"Number[]",
  				"Number?"
  			]
  		]
  	},
  	unhex: {
  		overloads: [
  			[
  				"String"
  			],
  			[
  				"String[]"
  			]
  		]
  	},
  	day: {
  		overloads: [
  			[
  			]
  		]
  	},
  	hour: {
  		overloads: [
  			[
  			]
  		]
  	},
  	minute: {
  		overloads: [
  			[
  			]
  		]
  	},
  	millis: {
  		overloads: [
  			[
  			]
  		]
  	},
  	month: {
  		overloads: [
  			[
  			]
  		]
  	},
  	second: {
  		overloads: [
  			[
  			]
  		]
  	},
  	year: {
  		overloads: [
  			[
  			]
  		]
  	},
  	nf: {
  		overloads: [
  			[
  				"Number|String",
  				"Integer|String?",
  				"Integer|String?"
  			],
  			[
  				"Number[]",
  				"Integer|String?",
  				"Integer|String?"
  			]
  		]
  	},
  	nfc: {
  		overloads: [
  			[
  				"Number|String",
  				"Integer|String?"
  			],
  			[
  				"Number[]",
  				"Integer|String?"
  			]
  		]
  	},
  	nfp: {
  		overloads: [
  			[
  				"Number",
  				"Integer?",
  				"Integer?"
  			],
  			[
  				"Number[]",
  				"Integer?",
  				"Integer?"
  			]
  		]
  	},
  	nfs: {
  		overloads: [
  			[
  				"Number",
  				"Integer?",
  				"Integer?"
  			],
  			[
  				"Array",
  				"Integer?",
  				"Integer?"
  			]
  		]
  	},
  	splitTokens: {
  		overloads: [
  			[
  				"String",
  				"String?"
  			]
  		]
  	},
  	shuffle: {
  		overloads: [
  			[
  				"Array",
  				"Boolean?"
  			]
  		]
  	},
  	strokeMode: {
  		overloads: [
  			[
  				"String"
  			]
  		]
  	},
  	buildGeometry: {
  		overloads: [
  			[
  				"Function"
  			]
  		]
  	},
  	freeGeometry: {
  		overloads: [
  			[
  				"p5.Geometry"
  			]
  		]
  	},
  	plane: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Integer?",
  				"Integer?"
  			]
  		]
  	},
  	box: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Number?",
  				"Integer?",
  				"Integer?"
  			]
  		]
  	},
  	sphere: {
  		overloads: [
  			[
  				"Number?",
  				"Integer?",
  				"Integer?"
  			]
  		]
  	},
  	cylinder: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Integer?",
  				"Integer?",
  				"Boolean?",
  				"Boolean?"
  			]
  		]
  	},
  	cone: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Integer?",
  				"Integer?",
  				"Boolean?"
  			]
  		]
  	},
  	ellipsoid: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Number?",
  				"Integer?",
  				"Integer?"
  			]
  		]
  	},
  	torus: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Integer?",
  				"Integer?"
  			]
  		]
  	},
  	curveDetail: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	orbitControl: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Number?",
  				"Object?"
  			]
  		]
  	},
  	debugMode: {
  		overloads: [
  			[
  			],
  			[
  				"GRID|AXES"
  			],
  			[
  				"GRID|AXES",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?"
  			],
  			[
  				"GRID|AXES",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?"
  			],
  			[
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	noDebugMode: {
  		overloads: [
  			[
  			]
  		]
  	},
  	ambientLight: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number?"
  			],
  			[
  				"Number",
  				"Number?"
  			],
  			[
  				"String"
  			],
  			[
  				"Number[]"
  			],
  			[
  				"p5.Color"
  			]
  		]
  	},
  	specularColor: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number"
  			],
  			[
  				"Number"
  			],
  			[
  				"String"
  			],
  			[
  				"Number[]"
  			],
  			[
  				"p5.Color"
  			]
  		]
  	},
  	directionalLight: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"p5.Vector"
  			],
  			[
  				"p5.Color|Number[]|String",
  				"Number",
  				"Number",
  				"Number"
  			],
  			[
  				"p5.Color|Number[]|String",
  				"p5.Vector"
  			]
  		]
  	},
  	pointLight: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"p5.Vector"
  			],
  			[
  				"p5.Color|Number[]|String",
  				"Number",
  				"Number",
  				"Number"
  			],
  			[
  				"p5.Color|Number[]|String",
  				"p5.Vector"
  			]
  		]
  	},
  	imageLight: {
  		overloads: [
  			[
  				"p5.Image"
  			]
  		]
  	},
  	panorama: {
  		overloads: [
  			[
  				"p5.Image"
  			]
  		]
  	},
  	lights: {
  		overloads: [
  			[
  			]
  		]
  	},
  	lightFalloff: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	spotLight: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number?",
  				"Number?"
  			],
  			[
  				"p5.Color|Number[]|String",
  				"p5.Vector",
  				"p5.Vector",
  				"Number?",
  				"Number?"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"p5.Vector",
  				"p5.Vector",
  				"Number?",
  				"Number?"
  			],
  			[
  				"p5.Color|Number[]|String",
  				"Number",
  				"Number",
  				"Number",
  				"p5.Vector",
  				"Number?",
  				"Number?"
  			],
  			[
  				"p5.Color|Number[]|String",
  				"p5.Vector",
  				"Number",
  				"Number",
  				"Number",
  				"Number?",
  				"Number?"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"p5.Vector",
  				"Number?",
  				"Number?"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"p5.Vector",
  				"Number",
  				"Number",
  				"Number",
  				"Number?",
  				"Number?"
  			],
  			[
  				"p5.Color|Number[]|String",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	noLights: {
  		overloads: [
  			[
  			]
  		]
  	},
  	loadModel: {
  		overloads: [
  			[
  				"String|Request",
  				"String?",
  				"Boolean?",
  				"function(p5.Geometry)?",
  				"function(Event)?"
  			],
  			[
  				"String|Request",
  				"String?",
  				"function(p5.Geometry)?",
  				"function(Event)?"
  			],
  			[
  				"String|Request",
  				"Object?"
  			]
  		]
  	},
  	model: {
  		overloads: [
  			[
  				"p5.Geometry",
  				"Number?"
  			]
  		]
  	},
  	createModel: {
  		overloads: [
  			[
  				"String",
  				"String?",
  				"Boolean?",
  				"function(p5.Geometry)?",
  				"function(Event)?"
  			],
  			[
  				"String",
  				"String?",
  				"function(p5.Geometry)?",
  				"function(Event)?"
  			],
  			[
  				"String",
  				"String?",
  				"Object?"
  			]
  		]
  	},
  	loadShader: {
  		overloads: [
  			[
  				"String|Request",
  				"String|Request",
  				"Function?",
  				"Function?"
  			]
  		]
  	},
  	createShader: {
  		overloads: [
  			[
  				"String",
  				"String",
  				"Object?"
  			]
  		]
  	},
  	loadFilterShader: {
  		overloads: [
  			[
  				"String",
  				"Function?",
  				"Function?"
  			]
  		]
  	},
  	buildFilterShader: {
  		overloads: [
  			[
  				"Function"
  			],
  			[
  				"Object"
  			]
  		]
  	},
  	createFilterShader: {
  		overloads: [
  			[
  				"String"
  			]
  		]
  	},
  	shader: {
  		overloads: [
  			[
  				"p5.Shader"
  			]
  		]
  	},
  	strokeShader: {
  		overloads: [
  			[
  				"p5.Shader"
  			]
  		]
  	},
  	imageShader: {
  		overloads: [
  			[
  				"p5.Shader"
  			]
  		]
  	},
  	buildMaterialShader: {
  		overloads: [
  			[
  				"Function"
  			],
  			[
  				"Object"
  			]
  		]
  	},
  	loadMaterialShader: {
  		overloads: [
  			[
  				"String",
  				"Function?",
  				"Function?"
  			]
  		]
  	},
  	baseMaterialShader: {
  		overloads: [
  			[
  			]
  		]
  	},
  	baseFilterShader: {
  		overloads: [
  			[
  			]
  		]
  	},
  	buildNormalShader: {
  		overloads: [
  			[
  				"Function"
  			],
  			[
  				"Object"
  			]
  		]
  	},
  	loadNormalShader: {
  		overloads: [
  			[
  				"String",
  				"Function?",
  				"Function?"
  			]
  		]
  	},
  	baseNormalShader: {
  		overloads: [
  			[
  			]
  		]
  	},
  	buildColorShader: {
  		overloads: [
  			[
  				"Function"
  			],
  			[
  				"Object"
  			]
  		]
  	},
  	loadColorShader: {
  		overloads: [
  			[
  				"String",
  				"Function?",
  				"Function?"
  			]
  		]
  	},
  	baseColorShader: {
  		overloads: [
  			[
  			]
  		]
  	},
  	buildStrokeShader: {
  		overloads: [
  			[
  				"Function"
  			],
  			[
  				"Object"
  			]
  		]
  	},
  	loadStrokeShader: {
  		overloads: [
  			[
  				"String",
  				"Function?",
  				"Function?"
  			]
  		]
  	},
  	baseStrokeShader: {
  		overloads: [
  			[
  			]
  		]
  	},
  	resetShader: {
  		overloads: [
  			[
  			]
  		]
  	},
  	texture: {
  		overloads: [
  			[
  				"p5.Image|p5.MediaElement|p5.Graphics|p5.Texture|p5.Framebuffer|p5.FramebufferTexture"
  			]
  		]
  	},
  	textureMode: {
  		overloads: [
  			[
  				"IMAGE|NORMAL"
  			]
  		]
  	},
  	textureWrap: {
  		overloads: [
  			[
  				"CLAMP|REPEAT|MIRROR",
  				"CLAMP|REPEAT|MIRROR?"
  			]
  		]
  	},
  	normalMaterial: {
  		overloads: [
  			[
  			]
  		]
  	},
  	ambientMaterial: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number"
  			],
  			[
  				"Number"
  			],
  			[
  				"p5.Color|Number[]|String"
  			]
  		]
  	},
  	emissiveMaterial: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number?"
  			],
  			[
  				"Number"
  			],
  			[
  				"p5.Color|Number[]|String"
  			]
  		]
  	},
  	specularMaterial: {
  		overloads: [
  			[
  				"Number",
  				"Number?"
  			],
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number?"
  			],
  			[
  				"p5.Color|Number[]|String"
  			]
  		]
  	},
  	shininess: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	metalness: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	roll: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	camera: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	perspective: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	linePerspective: {
  		overloads: [
  			[
  				"Boolean"
  			],
  			[
  			]
  		]
  	},
  	ortho: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	frustum: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	createCamera: {
  		overloads: [
  			[
  			]
  		]
  	},
  	setCamera: {
  		overloads: [
  			[
  				"p5.Camera"
  			]
  		]
  	},
  	saveObj: {
  		overloads: [
  			[
  				"String?"
  			]
  		]
  	},
  	saveStl: {
  		overloads: [
  			[
  				"String?",
  				"Object?"
  			]
  		]
  	},
  	fromAxisAngle: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	mult: {
  		overloads: [
  			[
  				"p5.Quat?"
  			]
  		]
  	},
  	rotateBy: {
  		overloads: [
  			[
  				"p5.Quat?"
  			]
  		]
  	},
  	setAttributes: {
  		overloads: [
  			[
  				"String",
  				"Boolean"
  			],
  			[
  				"Object"
  			]
  		]
  	}
  };
  var dataDoc = {
  	p5: p5$1,
  	"p5.Color": {
  	toString: {
  		overloads: [
  			[
  				"String?"
  			]
  		]
  	},
  	contrast: {
  		overloads: [
  			[
  				"Color"
  			]
  		]
  	},
  	setRed: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	setGreen: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	setBlue: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	setAlpha: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	}
  },
  	"p5.Graphics": {
  	reset: {
  		overloads: [
  			[
  			]
  		]
  	},
  	remove: {
  		overloads: [
  			[
  			]
  		]
  	},
  	createFramebuffer: {
  		overloads: [
  			[
  				"Object?"
  			]
  		]
  	}
  },
  	"p5.Element": {
  	remove: {
  		overloads: [
  			[
  			]
  		]
  	},
  	parent: {
  		overloads: [
  			[
  				"String|p5.Element|Object"
  			],
  			[
  			]
  		]
  	},
  	child: {
  		overloads: [
  			[
  			],
  			[
  				"String|p5.Element?"
  			]
  		]
  	},
  	html: {
  		overloads: [
  			[
  			],
  			[
  				"String?",
  				"Boolean?"
  			]
  		]
  	},
  	id: {
  		overloads: [
  			[
  				"String"
  			],
  			[
  			]
  		]
  	},
  	"class": {
  		overloads: [
  			[
  				"String"
  			],
  			[
  			]
  		]
  	},
  	addClass: {
  		overloads: [
  			[
  				"String"
  			]
  		]
  	},
  	removeClass: {
  		overloads: [
  			[
  				"String"
  			]
  		]
  	},
  	hasClass: {
  		overloads: [
  			[
  				null
  			]
  		]
  	},
  	toggleClass: {
  		overloads: [
  			[
  				null
  			]
  		]
  	},
  	center: {
  		overloads: [
  			[
  				"String?"
  			]
  		]
  	},
  	position: {
  		overloads: [
  			[
  			],
  			[
  				"Number?",
  				"Number?",
  				"String?"
  			]
  		]
  	},
  	show: {
  		overloads: [
  			[
  			]
  		]
  	},
  	hide: {
  		overloads: [
  			[
  			]
  		]
  	},
  	size: {
  		overloads: [
  			[
  			],
  			[
  				"Number|AUTO?",
  				"Number|AUTO?"
  			]
  		]
  	},
  	style: {
  		overloads: [
  			[
  				"String"
  			],
  			[
  				"String",
  				"String|p5.Color"
  			]
  		]
  	},
  	attribute: {
  		overloads: [
  			[
  			],
  			[
  				"String",
  				"String"
  			]
  		]
  	},
  	removeAttribute: {
  		overloads: [
  			[
  				"String"
  			]
  		]
  	},
  	value: {
  		overloads: [
  			[
  			],
  			[
  				"String|Number"
  			]
  		]
  	},
  	mousePressed: {
  		overloads: [
  			[
  				"Function|Boolean"
  			]
  		]
  	},
  	doubleClicked: {
  		overloads: [
  			[
  				"Function|Boolean"
  			]
  		]
  	},
  	mouseWheel: {
  		overloads: [
  			[
  				"Function|Boolean"
  			]
  		]
  	},
  	mouseReleased: {
  		overloads: [
  			[
  				"Function|Boolean"
  			]
  		]
  	},
  	mouseClicked: {
  		overloads: [
  			[
  				"Function|Boolean"
  			]
  		]
  	},
  	mouseMoved: {
  		overloads: [
  			[
  				"Function|Boolean"
  			]
  		]
  	},
  	mouseOver: {
  		overloads: [
  			[
  				"Function|Boolean"
  			]
  		]
  	},
  	mouseOut: {
  		overloads: [
  			[
  				"Function|Boolean"
  			]
  		]
  	},
  	dragOver: {
  		overloads: [
  			[
  				"Function|Boolean"
  			]
  		]
  	},
  	dragLeave: {
  		overloads: [
  			[
  				"Function|Boolean"
  			]
  		]
  	},
  	changed: {
  		overloads: [
  			[
  				"Function|Boolean"
  			]
  		]
  	},
  	input: {
  		overloads: [
  			[
  				"Function|Boolean"
  			]
  		]
  	},
  	drop: {
  		overloads: [
  			[
  				"Function",
  				"Function?"
  			]
  		]
  	},
  	draggable: {
  		overloads: [
  			[
  				"p5.Element?"
  			]
  		]
  	}
  },
  	"p5.MediaElement": {
  	play: {
  		overloads: [
  			[
  			]
  		]
  	},
  	stop: {
  		overloads: [
  			[
  			]
  		]
  	},
  	pause: {
  		overloads: [
  			[
  			]
  		]
  	},
  	loop: {
  		overloads: [
  			[
  			]
  		]
  	},
  	noLoop: {
  		overloads: [
  			[
  			]
  		]
  	},
  	autoplay: {
  		overloads: [
  			[
  				"Boolean?"
  			]
  		]
  	},
  	volume: {
  		overloads: [
  			[
  			],
  			[
  				"Number"
  			]
  		]
  	},
  	speed: {
  		overloads: [
  			[
  			],
  			[
  				"Number"
  			]
  		]
  	},
  	time: {
  		overloads: [
  			[
  				"Number?"
  			]
  		]
  	},
  	duration: {
  		overloads: [
  			[
  			]
  		]
  	},
  	onended: {
  		overloads: [
  			[
  				"Function"
  			]
  		]
  	},
  	connect: {
  		overloads: [
  			[
  				"AudioNode|Object"
  			]
  		]
  	},
  	disconnect: {
  		overloads: [
  			[
  			]
  		]
  	},
  	showControls: {
  		overloads: [
  			[
  			]
  		]
  	},
  	hideControls: {
  		overloads: [
  			[
  			]
  		]
  	},
  	addCue: {
  		overloads: [
  			[
  				"Number",
  				"Function",
  				"Object?"
  			]
  		]
  	},
  	removeCue: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	clearCues: {
  		overloads: [
  			[
  			]
  		]
  	}
  },
  	"p5.Image": {
  	pixelDensity: {
  		overloads: [
  			[
  				"Number?"
  			]
  		]
  	},
  	loadPixels: {
  		overloads: [
  			[
  			]
  		]
  	},
  	updatePixels: {
  		overloads: [
  			[
  				"Integer?",
  				"Integer?",
  				"Integer?",
  				"Integer?"
  			]
  		]
  	},
  	get: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			],
  			[
  			],
  			[
  				"Number",
  				"Number"
  			]
  		]
  	},
  	set: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number|Number[]|Object"
  			]
  		]
  	},
  	resize: {
  		overloads: [
  			[
  				"Number",
  				"Number"
  			]
  		]
  	},
  	copy: {
  		overloads: [
  			[
  				"p5.Image|p5.Element",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer"
  			],
  			[
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer"
  			]
  		]
  	},
  	mask: {
  		overloads: [
  			[
  				"p5.Image"
  			]
  		]
  	},
  	filter: {
  		overloads: [
  			[
  				"THRESHOLD|GRAY|OPAQUE|INVERT|POSTERIZE|ERODE|DILATE|BLUR",
  				"Number?"
  			]
  		]
  	},
  	blend: {
  		overloads: [
  			[
  				"p5.Image",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL"
  			],
  			[
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"Integer",
  				"BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL"
  			]
  		]
  	},
  	save: {
  		overloads: [
  			[
  				"String",
  				"String?"
  			]
  		]
  	},
  	reset: {
  		overloads: [
  			[
  			]
  		]
  	},
  	getCurrentFrame: {
  		overloads: [
  			[
  			]
  		]
  	},
  	setFrame: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	numFrames: {
  		overloads: [
  			[
  			]
  		]
  	},
  	play: {
  		overloads: [
  			[
  			]
  		]
  	},
  	pause: {
  		overloads: [
  			[
  			]
  		]
  	},
  	delay: {
  		overloads: [
  			[
  				"Number",
  				"Number?"
  			]
  		]
  	}
  },
  	"p5.Table": {
  	addRow: {
  		overloads: [
  			[
  				"p5.TableRow?"
  			]
  		]
  	},
  	removeRow: {
  		overloads: [
  			[
  				"Integer"
  			]
  		]
  	},
  	getRow: {
  		overloads: [
  			[
  				"Integer"
  			]
  		]
  	},
  	getRows: {
  		overloads: [
  			[
  			]
  		]
  	},
  	findRow: {
  		overloads: [
  			[
  				"String",
  				"Integer|String"
  			]
  		]
  	},
  	findRows: {
  		overloads: [
  			[
  				"String",
  				"Integer|String"
  			]
  		]
  	},
  	matchRow: {
  		overloads: [
  			[
  				"String|RegExp",
  				"String|Integer"
  			]
  		]
  	},
  	matchRows: {
  		overloads: [
  			[
  				"String",
  				"String|Integer?"
  			]
  		]
  	},
  	getColumn: {
  		overloads: [
  			[
  				"String|Number"
  			]
  		]
  	},
  	clearRows: {
  		overloads: [
  			[
  			]
  		]
  	},
  	addColumn: {
  		overloads: [
  			[
  				"String?"
  			]
  		]
  	},
  	getColumnCount: {
  		overloads: [
  			[
  			]
  		]
  	},
  	getRowCount: {
  		overloads: [
  			[
  			]
  		]
  	},
  	removeTokens: {
  		overloads: [
  			[
  				"String",
  				"String|Integer?"
  			]
  		]
  	},
  	trim: {
  		overloads: [
  			[
  				"String|Integer?"
  			]
  		]
  	},
  	removeColumn: {
  		overloads: [
  			[
  				"String|Integer"
  			]
  		]
  	},
  	set: {
  		overloads: [
  			[
  				"Integer",
  				"String|Integer",
  				"String|Number"
  			]
  		]
  	},
  	setNum: {
  		overloads: [
  			[
  				"Integer",
  				"String|Integer",
  				"Number"
  			]
  		]
  	},
  	setString: {
  		overloads: [
  			[
  				"Integer",
  				"String|Integer",
  				"String"
  			]
  		]
  	},
  	get: {
  		overloads: [
  			[
  				"Integer",
  				"String|Integer"
  			]
  		]
  	},
  	getNum: {
  		overloads: [
  			[
  				"Integer",
  				"String|Integer"
  			]
  		]
  	},
  	getString: {
  		overloads: [
  			[
  				"Integer",
  				"String|Integer"
  			]
  		]
  	},
  	getObject: {
  		overloads: [
  			[
  				"String?"
  			]
  		]
  	},
  	getArray: {
  		overloads: [
  			[
  			]
  		]
  	}
  },
  	"p5.TableRow": {
  	set: {
  		overloads: [
  			[
  				"String|Integer",
  				"String|Number"
  			]
  		]
  	},
  	setNum: {
  		overloads: [
  			[
  				"String|Integer",
  				"Number|String"
  			]
  		]
  	},
  	setString: {
  		overloads: [
  			[
  				"String|Integer",
  				"String|Number|Boolean|Object"
  			]
  		]
  	},
  	get: {
  		overloads: [
  			[
  				"String|Integer"
  			]
  		]
  	},
  	getNum: {
  		overloads: [
  			[
  				"String|Integer"
  			]
  		]
  	},
  	getString: {
  		overloads: [
  			[
  				"String|Integer"
  			]
  		]
  	}
  },
  	"p5.XML": {
  	getParent: {
  		overloads: [
  			[
  			]
  		]
  	},
  	getName: {
  		overloads: [
  			[
  			]
  		]
  	},
  	setName: {
  		overloads: [
  			[
  				"String"
  			]
  		]
  	},
  	hasChildren: {
  		overloads: [
  			[
  			]
  		]
  	},
  	listChildren: {
  		overloads: [
  			[
  			]
  		]
  	},
  	getChildren: {
  		overloads: [
  			[
  				"String?"
  			]
  		]
  	},
  	getChild: {
  		overloads: [
  			[
  				"String|Integer"
  			]
  		]
  	},
  	addChild: {
  		overloads: [
  			[
  				"p5.XML"
  			]
  		]
  	},
  	removeChild: {
  		overloads: [
  			[
  				"String|Integer"
  			]
  		]
  	},
  	getAttributeCount: {
  		overloads: [
  			[
  			]
  		]
  	},
  	listAttributes: {
  		overloads: [
  			[
  			]
  		]
  	},
  	hasAttribute: {
  		overloads: [
  			[
  				"String"
  			]
  		]
  	},
  	getNum: {
  		overloads: [
  			[
  				"String",
  				"Number?"
  			]
  		]
  	},
  	getString: {
  		overloads: [
  			[
  				"String",
  				"Number?"
  			]
  		]
  	},
  	setAttribute: {
  		overloads: [
  			[
  				"String",
  				"Number|String|Boolean"
  			]
  		]
  	},
  	getContent: {
  		overloads: [
  			[
  				"String?"
  			]
  		]
  	},
  	serialize: {
  		overloads: [
  			[
  			]
  		]
  	}
  },
  	"p5.Vector": {
  	getValue: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	setValue: {
  		overloads: [
  			[
  				"Number",
  				"Number"
  			]
  		]
  	},
  	set: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Number?"
  			],
  			[
  				"p5.Vector|Number[]"
  			]
  		]
  	},
  	copy: {
  		overloads: [
  			[
  			]
  		]
  	},
  	add: {
  		overloads: [
  			[
  				"Number|Array",
  				"Number?",
  				"Number?"
  			],
  			[
  				"p5.Vector|Number[]"
  			]
  		]
  	},
  	rem: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number"
  			],
  			[
  				"p5.Vector|Number[]"
  			]
  		]
  	},
  	sub: {
  		overloads: [
  			[
  				"Number",
  				"Number?",
  				"Number?"
  			],
  			[
  				"p5.Vector|Number[]"
  			]
  		]
  	},
  	mult: {
  		overloads: [
  			[
  				"Number"
  			],
  			[
  				"Number",
  				"Number",
  				"Number?"
  			],
  			[
  				"Number[]"
  			],
  			[
  				"p5.Vector"
  			]
  		]
  	},
  	div: {
  		overloads: [
  			[
  				"Number"
  			],
  			[
  				"Number",
  				"Number",
  				"Number?"
  			],
  			[
  				"Number[]"
  			],
  			[
  				"p5.Vector"
  			]
  		]
  	},
  	mag: {
  		overloads: [
  			[
  			]
  		]
  	},
  	magSq: {
  		overloads: [
  			[
  			]
  		]
  	},
  	dot: {
  		overloads: [
  			[
  				"Number",
  				"Number?",
  				"Number?"
  			],
  			[
  				"p5.Vector"
  			]
  		]
  	},
  	cross: {
  		overloads: [
  			[
  				"p5.Vector"
  			]
  		]
  	},
  	dist: {
  		overloads: [
  			[
  				"p5.Vector"
  			]
  		]
  	},
  	normalize: {
  		overloads: [
  			[
  			]
  		]
  	},
  	limit: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	setMag: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	heading: {
  		overloads: [
  			[
  			]
  		]
  	},
  	setHeading: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	rotate: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	angleBetween: {
  		overloads: [
  			[
  				"p5.Vector"
  			]
  		]
  	},
  	lerp: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			],
  			[
  				"p5.Vector",
  				"Number"
  			]
  		]
  	},
  	slerp: {
  		overloads: [
  			[
  				"p5.Vector",
  				"Number"
  			]
  		]
  	},
  	reflect: {
  		overloads: [
  			[
  				"p5.Vector"
  			]
  		]
  	},
  	array: {
  		overloads: [
  			[
  			]
  		]
  	},
  	equals: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Number?"
  			],
  			[
  				"p5.Vector|Array"
  			]
  		]
  	},
  	clampToZero: {
  		overloads: [
  			[
  			]
  		]
  	},
  	fromAngle: {
  		overloads: [
  			[
  				"Number",
  				"Number?"
  			]
  		]
  	},
  	fromAngles: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number?"
  			]
  		]
  	},
  	random2D: {
  		overloads: [
  			[
  			]
  		]
  	},
  	random3D: {
  		overloads: [
  			[
  			]
  		]
  	}
  },
  	"p5.Font": {
  	textToPaths: {
  		overloads: [
  			[
  				"String",
  				"Number",
  				"Number",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	textToPoints: {
  		overloads: [
  			[
  				"String",
  				"Number",
  				"Number",
  				"Object?"
  			]
  		]
  	},
  	textToContours: {
  		overloads: [
  			[
  				"String",
  				"Number",
  				"Number",
  				"Object?"
  			]
  		]
  	},
  	textToModel: {
  		overloads: [
  			[
  				"String",
  				"Number",
  				"Number",
  				"Number",
  				"Number",
  				"Object?"
  			]
  		]
  	}
  },
  	"p5.Camera": {
  	perspective: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	ortho: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	frustum: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	pan: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	tilt: {
  		overloads: [
  			[
  				"Number"
  			]
  		]
  	},
  	lookAt: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	camera: {
  		overloads: [
  			[
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?",
  				"Number?"
  			]
  		]
  	},
  	move: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	setPosition: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number"
  			]
  		]
  	},
  	set: {
  		overloads: [
  			[
  				"p5.Camera"
  			]
  		]
  	},
  	slerp: {
  		overloads: [
  			[
  				"p5.Camera",
  				"p5.Camera",
  				"Number"
  			]
  		]
  	}
  },
  	"p5.Framebuffer": {
  	resize: {
  		overloads: [
  			[
  				"Number",
  				"Number"
  			]
  		]
  	},
  	pixelDensity: {
  		overloads: [
  			[
  				"Number?"
  			]
  		]
  	},
  	autoSized: {
  		overloads: [
  			[
  				"Boolean?"
  			]
  		]
  	},
  	createCamera: {
  		overloads: [
  			[
  			]
  		]
  	},
  	remove: {
  		overloads: [
  			[
  			]
  		]
  	},
  	begin: {
  		overloads: [
  			[
  			]
  		]
  	},
  	end: {
  		overloads: [
  			[
  			]
  		]
  	},
  	draw: {
  		overloads: [
  			[
  				"Function"
  			]
  		]
  	},
  	loadPixels: {
  		overloads: [
  			[
  			]
  		]
  	},
  	get: {
  		overloads: [
  			[
  				"Number",
  				"Number",
  				"Number",
  				"Number"
  			],
  			[
  			],
  			[
  				"Number",
  				"Number"
  			]
  		]
  	}
  },
  	"p5.Geometry": {
  	calculateBoundingBox: {
  		overloads: [
  			[
  			]
  		]
  	},
  	clearColors: {
  		overloads: [
  			[
  			]
  		]
  	},
  	flipU: {
  		overloads: [
  			[
  			]
  		]
  	},
  	computeFaces: {
  		overloads: [
  			[
  			]
  		]
  	},
  	computeNormals: {
  		overloads: [
  			[
  				"FLAT|SMOOTH?",
  				"Object?"
  			]
  		]
  	},
  	makeEdgesFromFaces: {
  		overloads: [
  			[
  			]
  		]
  	},
  	normalize: {
  		overloads: [
  			[
  			]
  		]
  	},
  	vertexProperty: {
  		overloads: [
  			[
  				"String",
  				"Number|Number[]",
  				"Number?"
  			]
  		]
  	},
  	flipV: {
  		overloads: [
  			[
  			]
  		]
  	}
  },
  	"p5.Shader": {
  	version: {
  		overloads: [
  			[
  			]
  		]
  	},
  	inspectHooks: {
  		overloads: [
  			[
  			]
  		]
  	},
  	modify: {
  		overloads: [
  			[
  				"Function",
  				"Object?"
  			],
  			[
  				"Object?"
  			]
  		]
  	},
  	copyToContext: {
  		overloads: [
  			[
  				"p5|p5.Graphics"
  			]
  		]
  	},
  	setUniform: {
  		overloads: [
  			[
  				"String",
  				"Boolean|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture"
  			]
  		]
  	}
  }
  };

  /**
   * @for p5
   * @requires core
   */

  function validateParams(p5, fn, lifecycles) {
    // Cache for Zod schemas
    let schemaRegistry = new Map();

    // Mapping names of p5 types to their constructor functions.
    // p5Constructors:
    //   - Color: f()
    //   - Graphics: f()
    //   - Vector: f()
    // and so on.
    // const p5Constructors = {};
    // NOTE: This is a tempt fix for unit test but is not correct
    // Attaced constructors are `undefined`
    const p5Constructors = Object.keys(p5).reduce((acc, val) => {
      if (
        val.match(/^[A-Z]/) && // Starts with a capital
        !val.match(/^[A-Z][A-Z0-9]*$/) && // Is not an all caps constant
        p5[val] instanceof Function // Is a function
      ) {
        acc[val] = p5[val];
      }
      return acc;
    }, {});

    function loadP5Constructors() {
      // Make a list of all p5 classes to be used for argument validation
      // This must be done only when everything has loaded otherwise we get
      // an empty array
      for (let key of Object.keys(p5)) {
        // Get a list of all constructors in p5. They are functions whose names
        // start with a capital letter
        if (typeof p5[key] === 'function' && key[0] !== key[0].toLowerCase()) {
          p5Constructors[key] = p5[key];
        }
      }
    }

    // `constantsMap` maps constants to their values, e.g.
    // {
    //   ADD: 'lighter',
    //   ALT: 18,
    //   ARROW: 'default',
    //   AUTO: 'auto',
    //   ...
    // }
    const constantsMap = {};
    for (const [key, value] of Object.entries(constants)) {
      constantsMap[key] = value;
    }

    // Start initializing `schemaMap` with primitive types. `schemaMap` will
    // eventually contain both primitive types and web API objects.
    const schemaMap = {
      'Any': any(),
      'Array': array(any()),
      'Boolean': boolean(),
      'Function': _function(),
      'Integer': number().int(),
      'Number': number(),
      'Object': object({}),
      'String': string()
    };

    const webAPIObjects = [
      'AudioNode',
      'HTMLCanvasElement',
      'HTMLElement',
      'KeyboardEvent',
      'MouseEvent',
      'RegExp',
      'TouchEvent',
      'UIEvent',
      'WheelEvent'
    ];

    function generateWebAPISchemas(apiObjects) {
      return apiObjects.reduce((acc, obj) => {
        acc[obj] = custom(data => data instanceof globalThis[obj], {
          message: `Expected a ${obj}`
        });
        return acc;
      }, {});
    }

    const webAPISchemas = generateWebAPISchemas(webAPIObjects);
    // Add web API schemas to the schema map.
    Object.assign(schemaMap, webAPISchemas);

    // For mapping 0-indexed parameters to their ordinal representation, e.g.
    // "first" for 0, "second" for 1, "third" for 2, etc.
    const ordinals = ['first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth', 'ninth', 'tenth'];

    function extractFuncNameAndClass(func) {
      const ichDot = func.lastIndexOf('.');
      const funcName = func.slice(ichDot + 1);
      const funcClass = func.slice(0, ichDot !== -1 ? ichDot : 0) || 'p5';
      return { funcName, funcClass };
    }

    function validBracketNesting(type) {
      let level = 0;
      for (let i = 0; i < type.length; i++) {
        if (type[i] === '[') {
          level++;
        } else if (type[i] === ']') {
          level--;
          if (level < 0) return false;
        }
      }
      return level === 0;
    }

    /**
     * This is a helper function that generates Zod schemas for a function based on
     * the parameter data from `docs/parameterData.json`.
     *
     * Example parameter data for function `background`:
     * "background": {
     *    "overloads": [
     *      ["p5.Color"],
     *      ["String", "Number?"],
     *      ["Number", "Number?"],
     *      ["Number", "Number", "Number", "Number?"],
     *      ["Number[]"],
     *      ["p5.Image", "Number?"]
     *    ]
     *  }
     * Where each array in `overloads` represents a set of valid overloaded
     * parameters, and `?` is a shorthand for `Optional`.
     *
     * @method generateZodSchemasForFunc
     * @private
     * @param {String} func - Name of the function. Expect global functions like `sin` and class methods like `p5.Vector.add`
     * @returns {z.ZodSchema} Zod schema
     */
    const generateZodSchemasForFunc = function (func) {
      const { funcName, funcClass } = extractFuncNameAndClass(func);
      let funcInfo = dataDoc[funcClass][funcName];

      if(!funcInfo) return;

      let overloads = [];
      if (funcInfo.hasOwnProperty('overloads')) {
        overloads = funcInfo.overloads;
      }

      // Returns a schema for a single type, i.e. z.boolean() for `boolean`.
      const generateTypeSchema = baseType => {
        if (!baseType) return any();

        let typeSchema;

        // Check for constants. Note that because we're ultimately interested in the value of
        // the constant, mapping constants to their values via `constantsMap` is
        // necessary.
        if (baseType in constantsMap) {
          typeSchema = literal$1(constantsMap[baseType]);
        }
        // Some more constants are attached directly to p5.prototype, e.g. by addons:
        else if (baseType.match(/^[A-Z][A-Z0-9]*$/) && baseType in fn) {
          typeSchema = literal$1(fn[baseType]);
        }
        // Function types
        else if (baseType.startsWith('function')) {
          typeSchema = _function();
        }
        // All p5 objects start with `p5` in the documentation, i.e. `p5.Camera`.
        else if (/^p5\.[a-zA-Z0-9]+$/.exec(baseType) || baseType === 'p5') {
          const className = baseType.substring(baseType.indexOf('.') + 1);
          typeSchema = _instanceof(p5Constructors[className]);
        }
        // For primitive types and web API objects.
        else if (schemaMap[baseType]) {
          typeSchema = schemaMap[baseType];
        }
        // Tuple types
        else if (
          baseType.startsWith('[') &&
          baseType.endsWith(']') &&
          validBracketNesting(baseType.slice(1, -1))
        ) {
          typeSchema = tuple(
            baseType
              .slice(1, -1)
              .split(/, */g)
              .map(entry => generateTypeSchema(entry))
          );
        }
        // JavaScript classes, e.g. Request
        else if (baseType.match(/^[A-Z]/) && baseType in window) {
          typeSchema = _instanceof(window[baseType]);
        }
        // Generate a schema for a single parameter that can be of multiple
        // types / constants, i.e. `String|Number|Array`.
        //
        // Here, z.union() is used over z.enum() (which seems more intuitive) for
        // constants for the following reasons:
        // 1) z.enum() only allows a fixed set of allowable string values. However,
        // our constants sometimes have numeric or non-primitive values.
        // 2) In some cases, the type can be constants or strings, making z.enum()
        // insufficient for the use case.
        else if (baseType.includes('|') && baseType.split('|').every(t => validBracketNesting(t))) {
          const types = baseType.split('|');
          typeSchema = union(types
            .map(t => generateTypeSchema(t))
            .filter(s => s !== undefined));
        } else if (baseType.endsWith('[]')) {
          typeSchema = array(generateTypeSchema(baseType.slice(0, -2)));
        } else {
          throw new Error(`Unsupported type '${baseType}' in parameter validation. Please report this issue.`);
        }

        return typeSchema;
      };

      // Generate a schema for a single parameter. In the case where a parameter can
      // be of multiple types, `generateTypeSchema` is called for each type.
      const generateParamSchema = param => {
        const isOptional = param?.endsWith('?');
        param = param?.replace(/\?$/, '');

        const isRest = param?.startsWith('...') && param?.endsWith('[]');
        param = param?.replace(/^\.\.\.(.+)\[\]$/, '$1');

        let schema = generateTypeSchema(param);
        // Fallback to z.custom() because function types are no longer
        // returns a Zod schema.
        if (schema.def.type === 'function') {
          schema = custom(val => val instanceof Function);
        }

        if (isOptional) {
          schema = schema.optional();
        }
        return { schema, rest: isRest };
      };

      // Note that in Zod, `optional()` only checks for undefined, not the absence
      // of value.
      //
      // Let's say we have a function with 3 parameters, and the last one is
      // optional, i.e. func(a, b, c?). If we only have a z.tuple() for the
      // parameters, where the third schema is optional, then we will only be able
      // to validate func(10, 10, undefined), but not func(10, 10), which is
      // a completely valid call.
      //
      // Therefore, on top of using `optional()`, we also have to generate parameter
      // combinations that are valid for all numbers of parameters.
      const generateOverloadCombinations = params => {
        // No optional parameters, return the original parameter list right away.
        if (!params.some(p => p?.endsWith('?'))) {
          return [params];
        }

        const requiredParamsCount = params.filter(p => p === null || !p.endsWith('?')).length;
        const result = [];

        for (let i = requiredParamsCount; i <= params.length; i++) {
          result.push(params.slice(0, i));
        }

        return result;
      };

      // Generate schemas for each function overload and merge them
      const overloadSchemas = overloads.flatMap(overload => {
        const combinations = generateOverloadCombinations(overload);

        return combinations.map(combo => {
          const params = combo
            .map(p => generateParamSchema(p))
            .filter(s => s.schema !== undefined);

          let rest;
          if (params.at(-1)?.rest) {
            rest = params.pop();
          }

          let combined = tuple(params.map(s => s.schema));
          if (rest) {
            combined = combined.rest(rest.schema);
          }
          return combined;
        });
      });

      return overloadSchemas.length === 1
        ? overloadSchemas[0]
        : union(overloadSchemas);
    };

    /**
     * Finds the closest schema to the input arguments.
     *
     * This is a helper function that identifies the closest schema to the input
     * arguments, in the case of an initial validation error. We will then use the
     * closest schema to generate a friendly error message.
     *
     * @private
     * @param {z.ZodSchema} schema - Zod schema.
     * @param {Array} args - User input arguments.
     * @returns {z.ZodSchema} Closest schema matching the input arguments.
     */
    const findClosestSchema = function (schema, args) {
      if (!(schema instanceof ZodUnion)) {
        return schema;
      }

      // Helper function that scores how close the input arguments are to a schema.
      // Lower score means closer match.
      const scoreSchema = schema => {
        let score = Infinity;
        if (!(schema instanceof ZodTuple)) {
          console.warn('Schema below is not a tuple: ');
          printZodSchema(schema);
          return score;
        }

        const numArgs = args.length;
        const schemaItems = schema.def.items;
        const numSchemaItems = schemaItems.length;
        const numRequiredSchemaItems = schemaItems
          .filter(item => !item.isOptional())
          .length;

        if (numArgs >= numRequiredSchemaItems && numArgs <= numSchemaItems) {
          score = 0;
        }
        // Here, give more weight to mismatch in number of arguments.
        //
        // For example, color() can either take [Number, Number?] or
        // [Number, Number, Number, Number?] as list of parameters.
        // If the user passed in 3 arguments, [10, undefined, undefined], it's
        // more than likely that they intended to pass in 3 arguments, but the
        // last two arguments are invalid.
        //
        // If there's no bias towards matching the number of arguments, the error
        // message will show that we're expecting at most 2 arguments, but more
        // are received.
        else {
          score = Math.abs(
            numArgs < numRequiredSchemaItems ?
              numRequiredSchemaItems - numArgs :
              numArgs - numSchemaItems
          ) * 4;
        }

        for (let i = 0; i < Math.min(schemaItems.length, args.length); i++) {
          const paramSchema = schemaItems[i];
          const arg = args[i];

          if (!paramSchema.safeParse(arg).success) score++;
        }

        return score;
      };

      // Default to the first schema, so that we are guaranteed to return a result.
      let closestSchema = schema.def.options[0];
      // We want to return the schema with the lowest score.
      let bestScore = Infinity;

      const schemaUnion = schema.def.options;
      schemaUnion.forEach(schema => {
        const score = scoreSchema(schema);
        if (score < bestScore) {
          closestSchema = schema;
          bestScore = score;
        }
      });

      return closestSchema;
    };

    /**
     * Prints a friendly error message after parameter validation, if validation
     * has failed.
     *
     * @method _friendlyParamError
     * @private
     * @param {z.ZodError} zodErrorObj - The Zod error object containing validation errors.
     * @param {String} func - Name of the function. Expect global functions like `sin` and class methods like `p5.Vector.add`
     * @returns {String} The friendly error message.
     */
    const friendlyParamError = function (zodErrorObj, func, args) {
      let message = '🌸 p5.js says: ';
      let isVersionError = false;
      // The `zodErrorObj` might contain multiple errors of equal importance
      // (after scoring the schema closeness in `findClosestSchema`). Here, we
      // always print the first error so that user can work through the errors
      // one by one.
      let currentError = zodErrorObj.issues[0];

      // Helper function to build a type mismatch message.
      const buildTypeMismatchMessage =
        (actualType, expectedTypeStr, position) => {
          const positionStr = position ? `at the ${ordinals[position]} parameter` : '';
          const actualTypeStr = actualType ? `, but received ${actualType}` : '';
          return `Expected ${expectedTypeStr} ${positionStr}${actualTypeStr}`;
        };

      // Union errors occur when a parameter can be of multiple types but is not
      // of any of them. In this case, aggregate all possible types and print
      // a friendly error message that indicates what the expected types are at
      // which position (position is not 0-indexed, for accessibility reasons).
      const processUnionError = error => {
        const expectedTypes = new Set();
        let actualType;

        error.errors.forEach(err => {
          const issue = err[0];
          if (issue) {
            if (!actualType) {
              actualType = issue.message;
            }

            if (issue.code === 'invalid_type') {
              actualType = issue.message.split(', received ')[1];
              expectedTypes.add(issue.expected);
            }
            // The case for constants. Since we don't want to print out the actual
            // constant values in the error message, the error message will
            // direct users to the documentation.
            else if (issue.code === 'invalid_value') {
              expectedTypes.add('constant (please refer to documentation for allowed values)');
              actualType = args[error.path[0]];
            } else if (issue.code === 'custom') {
              const match = issue.message.match(/Input not instance of (\w+)/);
              if (match) expectedTypes.add(match[1]);
              actualType = undefined;
            }
          }
        });

        if (expectedTypes.size > 0) {
          if (error.path?.length > 0 && args[error.path[0]] instanceof Promise)  {
            message += 'Did you mean to put `await` before a loading function? ' +
              'An unexpected Promise was found. ';
            isVersionError = true;
          }

          const expectedTypesStr = Array.from(expectedTypes).join(' or ');
          const position = error.path.join('.');

          message += buildTypeMismatchMessage(
            actualType, expectedTypesStr, position
          );
        }

        return message;
      };

      switch (currentError.code) {
        case 'invalid_union': {
          processUnionError(currentError);
          break;
        }
        case 'too_small': {
          const minArgs = currentError.minimum;
          message += `Expected at least ${minArgs} argument${minArgs > 1 ? 's' : ''}, but received fewer`;
          break;
        }
        case 'invalid_type': {
          message += buildTypeMismatchMessage(currentError.message.split(', received ')[1], currentError.expected, currentError.path.join('.'));
          break;
        }
        case 'too_big': {
          const maxArgs = currentError.maximum;
          message += `Expected at most ${maxArgs} argument${maxArgs > 1 ? 's' : ''}, but received more`;
          break;
        }
        default: {
          console.log('Zod error object', currentError);
        }
      }

      // Let the user know which function is generating the error.
      message += ` in ${func}().`;

      // Generates a link to the documentation based on the given function name.
      // TODO: Check if the link is reachable before appending it to the error
      // message.
      const generateDocumentationLink = func => {
        const { funcName, funcClass } = extractFuncNameAndClass(func);
        const p5BaseUrl = 'https://p5js.org/reference';
        const url = `${p5BaseUrl}/${funcClass}/${funcName}`;

        return url;
      };

      if (currentError.code === 'too_big' || currentError.code === 'too_small') {
        const documentationLink = generateDocumentationLink(func);
        message += ` For more information, see ${documentationLink}.`;
      }

      if (isVersionError) {
        p5._error(this, message);
      } else {
        console.log(message);
      }
      return message;
    };

    /**
     * Runs parameter validation by matching the input parameters to Zod schemas
     * generated from the parameter data from `docs/parameterData.json`.
     *
     * @private
     * @param {String} func - Name of the function.
     * @param {Array} args - User input arguments.
     * @returns {Object} The validation result.
     * @returns {Boolean} result.success - Whether the validation was successful.
     * @returns {any} [result.data] - The parsed data if validation was successful.
     * @returns {String} [result.error] - The validation error message if validation has failed.
     */
    const validate = function (func, args) {
      if (p5.disableFriendlyErrors) {
        return; // skip FES
      }

      if (!Array.isArray(args)) {
        args = Array.from(args);
      }

      // An edge case: even when all arguments are optional and therefore,
      // theoretically allowed to stay undefined and valid, it is likely that the
      // user intended to call the function with non-undefined arguments. Skip
      // regular workflow and return a friendly error message right away.
      if (
        Array.isArray(args) &&
        args.length > 0 &&
        args.every(arg => arg === undefined)
      ) {
        const undefinedErrorMessage = `🌸 p5.js says: All arguments for ${func}() are undefined. There is likely an error in the code.`;

        return {
          success: false,
          error: undefinedErrorMessage
        };
      }

      let funcSchemas = schemaRegistry.get(func);
      if (!funcSchemas) {
        funcSchemas = generateZodSchemasForFunc(func);
        if (!funcSchemas) return;
        schemaRegistry.set(func, funcSchemas);
      }

      try {
        return {
          success: true,
          data: funcSchemas.parse(args)
        };
      } catch (error) {
        const closestSchema = findClosestSchema(funcSchemas, args);
        const zodError = closestSchema.safeParse(args).error;
        const errorMessage = friendlyParamError(zodError, func, args);

        return {
          success: false,
          error: errorMessage
        };
      }
    };

    fn._validate = validate; // TEMP: For unit tests

    p5.decorateHelper(
      /^(?!_).+$/,
      function(target, { name }){
        return function(...args){
          if (!p5.disableFriendlyErrors && !p5.disableParameterValidator) {
            validate(name, args);
          }
          return target.apply(this, args);
        };
      }
    );

    lifecycles.presetup = function(){
      loadP5Constructors();
    };
  }

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

  // This file was generated. Do not modify manually!
  var astralIdentifierCodes = [509, 0, 227, 0, 150, 4, 294, 9, 1368, 2, 2, 1, 6, 3, 41, 2, 5, 0, 166, 1, 574, 3, 9, 9, 7, 9, 32, 4, 318, 1, 80, 3, 71, 10, 50, 3, 123, 2, 54, 14, 32, 10, 3, 1, 11, 3, 46, 10, 8, 0, 46, 9, 7, 2, 37, 13, 2, 9, 6, 1, 45, 0, 13, 2, 49, 13, 9, 3, 2, 11, 83, 11, 7, 0, 3, 0, 158, 11, 6, 9, 7, 3, 56, 1, 2, 6, 3, 1, 3, 2, 10, 0, 11, 1, 3, 6, 4, 4, 68, 8, 2, 0, 3, 0, 2, 3, 2, 4, 2, 0, 15, 1, 83, 17, 10, 9, 5, 0, 82, 19, 13, 9, 214, 6, 3, 8, 28, 1, 83, 16, 16, 9, 82, 12, 9, 9, 7, 19, 58, 14, 5, 9, 243, 14, 166, 9, 71, 5, 2, 1, 3, 3, 2, 0, 2, 1, 13, 9, 120, 6, 3, 6, 4, 0, 29, 9, 41, 6, 2, 3, 9, 0, 10, 10, 47, 15, 343, 9, 54, 7, 2, 7, 17, 9, 57, 21, 2, 13, 123, 5, 4, 0, 2, 1, 2, 6, 2, 0, 9, 9, 49, 4, 2, 1, 2, 4, 9, 9, 330, 3, 10, 1, 2, 0, 49, 6, 4, 4, 14, 10, 5350, 0, 7, 14, 11465, 27, 2343, 9, 87, 9, 39, 4, 60, 6, 26, 9, 535, 9, 470, 0, 2, 54, 8, 3, 82, 0, 12, 1, 19628, 1, 4178, 9, 519, 45, 3, 22, 543, 4, 4, 5, 9, 7, 3, 6, 31, 3, 149, 2, 1418, 49, 513, 54, 5, 49, 9, 0, 15, 0, 23, 4, 2, 14, 1361, 6, 2, 16, 3, 6, 2, 1, 2, 4, 101, 0, 161, 6, 10, 9, 357, 0, 62, 13, 499, 13, 245, 1, 2, 9, 726, 6, 110, 6, 6, 9, 4759, 9, 787719, 239];

  // This file was generated. Do not modify manually!
  var astralIdentifierStartCodes = [0, 11, 2, 25, 2, 18, 2, 1, 2, 14, 3, 13, 35, 122, 70, 52, 268, 28, 4, 48, 48, 31, 14, 29, 6, 37, 11, 29, 3, 35, 5, 7, 2, 4, 43, 157, 19, 35, 5, 35, 5, 39, 9, 51, 13, 10, 2, 14, 2, 6, 2, 1, 2, 10, 2, 14, 2, 6, 2, 1, 4, 51, 13, 310, 10, 21, 11, 7, 25, 5, 2, 41, 2, 8, 70, 5, 3, 0, 2, 43, 2, 1, 4, 0, 3, 22, 11, 22, 10, 30, 66, 18, 2, 1, 11, 21, 11, 25, 71, 55, 7, 1, 65, 0, 16, 3, 2, 2, 2, 28, 43, 28, 4, 28, 36, 7, 2, 27, 28, 53, 11, 21, 11, 18, 14, 17, 111, 72, 56, 50, 14, 50, 14, 35, 39, 27, 10, 22, 251, 41, 7, 1, 17, 2, 60, 28, 11, 0, 9, 21, 43, 17, 47, 20, 28, 22, 13, 52, 58, 1, 3, 0, 14, 44, 33, 24, 27, 35, 30, 0, 3, 0, 9, 34, 4, 0, 13, 47, 15, 3, 22, 0, 2, 0, 36, 17, 2, 24, 20, 1, 64, 6, 2, 0, 2, 3, 2, 14, 2, 9, 8, 46, 39, 7, 3, 1, 3, 21, 2, 6, 2, 1, 2, 4, 4, 0, 19, 0, 13, 4, 31, 9, 2, 0, 3, 0, 2, 37, 2, 0, 26, 0, 2, 0, 45, 52, 19, 3, 21, 2, 31, 47, 21, 1, 2, 0, 185, 46, 42, 3, 37, 47, 21, 0, 60, 42, 14, 0, 72, 26, 38, 6, 186, 43, 117, 63, 32, 7, 3, 0, 3, 7, 2, 1, 2, 23, 16, 0, 2, 0, 95, 7, 3, 38, 17, 0, 2, 0, 29, 0, 11, 39, 8, 0, 22, 0, 12, 45, 20, 0, 19, 72, 200, 32, 32, 8, 2, 36, 18, 0, 50, 29, 113, 6, 2, 1, 2, 37, 22, 0, 26, 5, 2, 1, 2, 31, 15, 0, 328, 18, 16, 0, 2, 12, 2, 33, 125, 0, 80, 921, 103, 110, 18, 195, 2637, 96, 16, 1071, 18, 5, 26, 3994, 6, 582, 6842, 29, 1763, 568, 8, 30, 18, 78, 18, 29, 19, 47, 17, 3, 32, 20, 6, 18, 433, 44, 212, 63, 129, 74, 6, 0, 67, 12, 65, 1, 2, 0, 29, 6135, 9, 1237, 42, 9, 8936, 3, 2, 6, 2, 1, 2, 290, 16, 0, 30, 2, 3, 0, 15, 3, 9, 395, 2309, 106, 6, 12, 4, 8, 8, 9, 5991, 84, 2, 70, 2, 1, 3, 0, 3, 1, 3, 3, 2, 11, 2, 0, 2, 6, 2, 64, 2, 3, 3, 7, 2, 6, 2, 27, 2, 3, 2, 4, 2, 0, 4, 6, 2, 339, 3, 24, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 7, 1845, 30, 7, 5, 262, 61, 147, 44, 11, 6, 17, 0, 322, 29, 19, 43, 485, 27, 229, 29, 3, 0, 496, 6, 2, 3, 2, 1, 2, 14, 2, 196, 60, 67, 8, 0, 1205, 3, 2, 26, 2, 1, 2, 0, 3, 0, 2, 9, 2, 3, 2, 0, 2, 0, 7, 0, 5, 0, 2, 0, 2, 0, 2, 2, 2, 1, 2, 0, 3, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 1, 2, 0, 3, 3, 2, 6, 2, 3, 2, 3, 2, 0, 2, 9, 2, 16, 6, 2, 2, 4, 2, 16, 4421, 42719, 33, 4153, 7, 221, 3, 5761, 15, 7472, 16, 621, 2467, 541, 1507, 4938, 6, 4191];

  // This file was generated. Do not modify manually!
  var nonASCIIidentifierChars = "\u200c\u200d\xb7\u0300-\u036f\u0387\u0483-\u0487\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u0669\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7\u06e8\u06ea-\u06ed\u06f0-\u06f9\u0711\u0730-\u074a\u07a6-\u07b0\u07c0-\u07c9\u07eb-\u07f3\u07fd\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u0897-\u089f\u08ca-\u08e1\u08e3-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962\u0963\u0966-\u096f\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09cb-\u09cd\u09d7\u09e2\u09e3\u09e6-\u09ef\u09fe\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a66-\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2\u0ae3\u0ae6-\u0aef\u0afa-\u0aff\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b55-\u0b57\u0b62\u0b63\u0b66-\u0b6f\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0be6-\u0bef\u0c00-\u0c04\u0c3c\u0c3e-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0c66-\u0c6f\u0c81-\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0ce6-\u0cef\u0cf3\u0d00-\u0d03\u0d3b\u0d3c\u0d3e-\u0d44\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0d62\u0d63\u0d66-\u0d6f\u0d81-\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0de6-\u0def\u0df2\u0df3\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0e50-\u0e59\u0eb1\u0eb4-\u0ebc\u0ec8-\u0ece\u0ed0-\u0ed9\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f3e\u0f3f\u0f71-\u0f84\u0f86\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102b-\u103e\u1040-\u1049\u1056-\u1059\u105e-\u1060\u1062-\u1064\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f-\u109d\u135d-\u135f\u1369-\u1371\u1712-\u1715\u1732-\u1734\u1752\u1753\u1772\u1773\u17b4-\u17d3\u17dd\u17e0-\u17e9\u180b-\u180d\u180f-\u1819\u18a9\u1920-\u192b\u1930-\u193b\u1946-\u194f\u19d0-\u19da\u1a17-\u1a1b\u1a55-\u1a5e\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1ab0-\u1abd\u1abf-\u1ace\u1b00-\u1b04\u1b34-\u1b44\u1b50-\u1b59\u1b6b-\u1b73\u1b80-\u1b82\u1ba1-\u1bad\u1bb0-\u1bb9\u1be6-\u1bf3\u1c24-\u1c37\u1c40-\u1c49\u1c50-\u1c59\u1cd0-\u1cd2\u1cd4-\u1ce8\u1ced\u1cf4\u1cf7-\u1cf9\u1dc0-\u1dff\u200c\u200d\u203f\u2040\u2054\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302f\u3099\u309a\u30fb\ua620-\ua629\ua66f\ua674-\ua67d\ua69e\ua69f\ua6f0\ua6f1\ua802\ua806\ua80b\ua823-\ua827\ua82c\ua880\ua881\ua8b4-\ua8c5\ua8d0-\ua8d9\ua8e0-\ua8f1\ua8ff-\ua909\ua926-\ua92d\ua947-\ua953\ua980-\ua983\ua9b3-\ua9c0\ua9d0-\ua9d9\ua9e5\ua9f0-\ua9f9\uaa29-\uaa36\uaa43\uaa4c\uaa4d\uaa50-\uaa59\uaa7b-\uaa7d\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uaaeb-\uaaef\uaaf5\uaaf6\uabe3-\uabea\uabec\uabed\uabf0-\uabf9\ufb1e\ufe00-\ufe0f\ufe20-\ufe2f\ufe33\ufe34\ufe4d-\ufe4f\uff10-\uff19\uff3f\uff65";

  // This file was generated. Do not modify manually!
  var nonASCIIidentifierStartChars = "\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u037f\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u052f\u0531-\u0556\u0559\u0560-\u0588\u05d0-\u05ea\u05ef-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u0860-\u086a\u0870-\u0887\u0889-\u088e\u08a0-\u08c9\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u09fc\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0af9\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c39\u0c3d\u0c58-\u0c5a\u0c5d\u0c60\u0c61\u0c80\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cdd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d04-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d54-\u0d56\u0d5f-\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e86-\u0e8a\u0e8c-\u0ea3\u0ea5\u0ea7-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f5\u13f8-\u13fd\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f8\u1700-\u1711\u171f-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1878\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191e\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4c\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1c80-\u1c8a\u1c90-\u1cba\u1cbd-\u1cbf\u1ce9-\u1cec\u1cee-\u1cf3\u1cf5\u1cf6\u1cfa\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2118-\u211d\u2124\u2126\u2128\u212a-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309b-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312f\u3131-\u318e\u31a0-\u31bf\u31f0-\u31ff\u3400-\u4dbf\u4e00-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua69d\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua7cd\ua7d0\ua7d1\ua7d3\ua7d5-\ua7dc\ua7f2-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua8fd\ua8fe\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\ua9e0-\ua9e4\ua9e6-\ua9ef\ua9fa-\ua9fe\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa7e-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uab30-\uab5a\uab5c-\uab69\uab70-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc";

  // These are a run-length and offset encoded representation of the
  // >0xffff code points that are a valid part of identifiers. The
  // offset starts at 0x10000, and each pair of numbers represents an
  // offset to the next range, and then a size of the range.

  // Reserved word lists for various dialects of the language

  var reservedWords = {
    3: "abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized throws transient volatile",
    5: "class enum extends super const export import",
    6: "enum",
    strict: "implements interface let package private protected public static yield",
    strictBind: "eval arguments"
  };

  // And the keywords

  var ecma5AndLessKeywords = "break case catch continue debugger default do else finally for function if return switch throw try var while with null true false instanceof typeof void delete new in this";

  var keywords$1 = {
    5: ecma5AndLessKeywords,
    "5module": ecma5AndLessKeywords + " export import",
    6: ecma5AndLessKeywords + " const class extends export import super"
  };

  var keywordRelationalOperator = /^in(stanceof)?$/;

  // ## Character categories

  var nonASCIIidentifierStart = new RegExp("[" + nonASCIIidentifierStartChars + "]");
  var nonASCIIidentifier = new RegExp("[" + nonASCIIidentifierStartChars + nonASCIIidentifierChars + "]");

  // This has a complexity linear to the value of the code. The
  // assumption is that looking up astral identifier characters is
  // rare.
  function isInAstralSet(code, set) {
    var pos = 0x10000;
    for (var i = 0; i < set.length; i += 2) {
      pos += set[i];
      if (pos > code) { return false }
      pos += set[i + 1];
      if (pos >= code) { return true }
    }
    return false
  }

  // Test whether a given character code starts an identifier.

  function isIdentifierStart(code, astral) {
    if (code < 65) { return code === 36 }
    if (code < 91) { return true }
    if (code < 97) { return code === 95 }
    if (code < 123) { return true }
    if (code <= 0xffff) { return code >= 0xaa && nonASCIIidentifierStart.test(String.fromCharCode(code)) }
    if (astral === false) { return false }
    return isInAstralSet(code, astralIdentifierStartCodes)
  }

  // Test whether a given character is part of an identifier.

  function isIdentifierChar(code, astral) {
    if (code < 48) { return code === 36 }
    if (code < 58) { return true }
    if (code < 65) { return false }
    if (code < 91) { return true }
    if (code < 97) { return code === 95 }
    if (code < 123) { return true }
    if (code <= 0xffff) { return code >= 0xaa && nonASCIIidentifier.test(String.fromCharCode(code)) }
    if (astral === false) { return false }
    return isInAstralSet(code, astralIdentifierStartCodes) || isInAstralSet(code, astralIdentifierCodes)
  }

  // ## Token types

  // The assignment of fine-grained, information-carrying type objects
  // allows the tokenizer to store the information it has about a
  // token in a way that is very cheap for the parser to look up.

  // All token type variables start with an underscore, to make them
  // easy to recognize.

  // The `beforeExpr` property is used to disambiguate between regular
  // expressions and divisions. It is set on all token types that can
  // be followed by an expression (thus, a slash after them would be a
  // regular expression).
  //
  // The `startsExpr` property is used to check if the token ends a
  // `yield` expression. It is set on all token types that either can
  // directly start an expression (like a quotation mark) or can
  // continue an expression (like the body of a string).
  //
  // `isLoop` marks a keyword as starting a loop, which is important
  // to know when parsing a label, in order to allow or disallow
  // continue jumps to that label.

  var TokenType = function TokenType(label, conf) {
    if ( conf === void 0 ) conf = {};

    this.label = label;
    this.keyword = conf.keyword;
    this.beforeExpr = !!conf.beforeExpr;
    this.startsExpr = !!conf.startsExpr;
    this.isLoop = !!conf.isLoop;
    this.isAssign = !!conf.isAssign;
    this.prefix = !!conf.prefix;
    this.postfix = !!conf.postfix;
    this.binop = conf.binop || null;
    this.updateContext = null;
  };

  function binop(name, prec) {
    return new TokenType(name, {beforeExpr: true, binop: prec})
  }
  var beforeExpr = {beforeExpr: true}, startsExpr = {startsExpr: true};

  // Map keyword names to token types.

  var keywords = {};

  // Succinct definitions of keyword token types
  function kw(name, options) {
    if ( options === void 0 ) options = {};

    options.keyword = name;
    return keywords[name] = new TokenType(name, options)
  }

  var types$1 = {
    num: new TokenType("num", startsExpr),
    regexp: new TokenType("regexp", startsExpr),
    string: new TokenType("string", startsExpr),
    name: new TokenType("name", startsExpr),
    privateId: new TokenType("privateId", startsExpr),
    eof: new TokenType("eof"),

    // Punctuation token types.
    bracketL: new TokenType("[", {beforeExpr: true, startsExpr: true}),
    bracketR: new TokenType("]"),
    braceL: new TokenType("{", {beforeExpr: true, startsExpr: true}),
    braceR: new TokenType("}"),
    parenL: new TokenType("(", {beforeExpr: true, startsExpr: true}),
    parenR: new TokenType(")"),
    comma: new TokenType(",", beforeExpr),
    semi: new TokenType(";", beforeExpr),
    colon: new TokenType(":", beforeExpr),
    dot: new TokenType("."),
    question: new TokenType("?", beforeExpr),
    questionDot: new TokenType("?."),
    arrow: new TokenType("=>", beforeExpr),
    template: new TokenType("template"),
    invalidTemplate: new TokenType("invalidTemplate"),
    ellipsis: new TokenType("...", beforeExpr),
    backQuote: new TokenType("`", startsExpr),
    dollarBraceL: new TokenType("${", {beforeExpr: true, startsExpr: true}),

    // Operators. These carry several kinds of properties to help the
    // parser use them properly (the presence of these properties is
    // what categorizes them as operators).
    //
    // `binop`, when present, specifies that this operator is a binary
    // operator, and will refer to its precedence.
    //
    // `prefix` and `postfix` mark the operator as a prefix or postfix
    // unary operator.
    //
    // `isAssign` marks all of `=`, `+=`, `-=` etcetera, which act as
    // binary operators with a very low precedence, that should result
    // in AssignmentExpression nodes.

    eq: new TokenType("=", {beforeExpr: true, isAssign: true}),
    assign: new TokenType("_=", {beforeExpr: true, isAssign: true}),
    incDec: new TokenType("++/--", {prefix: true, postfix: true, startsExpr: true}),
    prefix: new TokenType("!/~", {beforeExpr: true, prefix: true, startsExpr: true}),
    logicalOR: binop("||", 1),
    logicalAND: binop("&&", 2),
    bitwiseOR: binop("|", 3),
    bitwiseXOR: binop("^", 4),
    bitwiseAND: binop("&", 5),
    equality: binop("==/!=/===/!==", 6),
    relational: binop("</>/<=/>=", 7),
    bitShift: binop("<</>>/>>>", 8),
    plusMin: new TokenType("+/-", {beforeExpr: true, binop: 9, prefix: true, startsExpr: true}),
    modulo: binop("%", 10),
    star: binop("*", 10),
    slash: binop("/", 10),
    starstar: new TokenType("**", {beforeExpr: true}),
    coalesce: binop("??", 1),

    // Keyword token types.
    _break: kw("break"),
    _case: kw("case", beforeExpr),
    _catch: kw("catch"),
    _continue: kw("continue"),
    _debugger: kw("debugger"),
    _default: kw("default", beforeExpr),
    _do: kw("do", {isLoop: true, beforeExpr: true}),
    _else: kw("else", beforeExpr),
    _finally: kw("finally"),
    _for: kw("for", {isLoop: true}),
    _function: kw("function", startsExpr),
    _if: kw("if"),
    _return: kw("return", beforeExpr),
    _switch: kw("switch"),
    _throw: kw("throw", beforeExpr),
    _try: kw("try"),
    _var: kw("var"),
    _const: kw("const"),
    _while: kw("while", {isLoop: true}),
    _with: kw("with"),
    _new: kw("new", {beforeExpr: true, startsExpr: true}),
    _this: kw("this", startsExpr),
    _super: kw("super", startsExpr),
    _class: kw("class", startsExpr),
    _extends: kw("extends", beforeExpr),
    _export: kw("export"),
    _import: kw("import", startsExpr),
    _null: kw("null", startsExpr),
    _true: kw("true", startsExpr),
    _false: kw("false", startsExpr),
    _in: kw("in", {beforeExpr: true, binop: 7}),
    _instanceof: kw("instanceof", {beforeExpr: true, binop: 7}),
    _typeof: kw("typeof", {beforeExpr: true, prefix: true, startsExpr: true}),
    _void: kw("void", {beforeExpr: true, prefix: true, startsExpr: true}),
    _delete: kw("delete", {beforeExpr: true, prefix: true, startsExpr: true})
  };

  // Matches a whole line break (where CRLF is considered a single
  // line break). Used to count lines.

  var lineBreak = /\r\n?|\n|\u2028|\u2029/;
  var lineBreakG = new RegExp(lineBreak.source, "g");

  function isNewLine(code) {
    return code === 10 || code === 13 || code === 0x2028 || code === 0x2029
  }

  function nextLineBreak(code, from, end) {
    if ( end === void 0 ) end = code.length;

    for (var i = from; i < end; i++) {
      var next = code.charCodeAt(i);
      if (isNewLine(next))
        { return i < end - 1 && next === 13 && code.charCodeAt(i + 1) === 10 ? i + 2 : i + 1 }
    }
    return -1
  }

  var nonASCIIwhitespace = /[\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff]/;

  var skipWhiteSpace = /(?:\s|\/\/.*|\/\*[^]*?\*\/)*/g;

  var ref = Object.prototype;
  var hasOwnProperty = ref.hasOwnProperty;
  var toString$1 = ref.toString;

  var hasOwn = Object.hasOwn || (function (obj, propName) { return (
    hasOwnProperty.call(obj, propName)
  ); });

  var isArray = Array.isArray || (function (obj) { return (
    toString$1.call(obj) === "[object Array]"
  ); });

  var regexpCache = Object.create(null);

  function wordsRegexp(words) {
    return regexpCache[words] || (regexpCache[words] = new RegExp("^(?:" + words.replace(/ /g, "|") + ")$"))
  }

  function codePointToString(code) {
    // UTF-16 Decoding
    if (code <= 0xFFFF) { return String.fromCharCode(code) }
    code -= 0x10000;
    return String.fromCharCode((code >> 10) + 0xD800, (code & 1023) + 0xDC00)
  }

  var loneSurrogate = /(?:[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/;

  // These are used when `options.locations` is on, for the
  // `startLoc` and `endLoc` properties.

  var Position = function Position(line, col) {
    this.line = line;
    this.column = col;
  };

  Position.prototype.offset = function offset (n) {
    return new Position(this.line, this.column + n)
  };

  var SourceLocation = function SourceLocation(p, start, end) {
    this.start = start;
    this.end = end;
    if (p.sourceFile !== null) { this.source = p.sourceFile; }
  };

  // The `getLineInfo` function is mostly useful when the
  // `locations` option is off (for performance reasons) and you
  // want to find the line/column position for a given character
  // offset. `input` should be the code string that the offset refers
  // into.

  function getLineInfo(input, offset) {
    for (var line = 1, cur = 0;;) {
      var nextBreak = nextLineBreak(input, cur, offset);
      if (nextBreak < 0) { return new Position(line, offset - cur) }
      ++line;
      cur = nextBreak;
    }
  }

  // A second argument must be given to configure the parser process.
  // These options are recognized (only `ecmaVersion` is required):

  var defaultOptions = {
    // `ecmaVersion` indicates the ECMAScript version to parse. Must be
    // either 3, 5, 6 (or 2015), 7 (2016), 8 (2017), 9 (2018), 10
    // (2019), 11 (2020), 12 (2021), 13 (2022), 14 (2023), or `"latest"`
    // (the latest version the library supports). This influences
    // support for strict mode, the set of reserved words, and support
    // for new syntax features.
    ecmaVersion: null,
    // `sourceType` indicates the mode the code should be parsed in.
    // Can be either `"script"` or `"module"`. This influences global
    // strict mode and parsing of `import` and `export` declarations.
    sourceType: "script",
    // `onInsertedSemicolon` can be a callback that will be called when
    // a semicolon is automatically inserted. It will be passed the
    // position of the inserted semicolon as an offset, and if
    // `locations` is enabled, it is given the location as a `{line,
    // column}` object as second argument.
    onInsertedSemicolon: null,
    // `onTrailingComma` is similar to `onInsertedSemicolon`, but for
    // trailing commas.
    onTrailingComma: null,
    // By default, reserved words are only enforced if ecmaVersion >= 5.
    // Set `allowReserved` to a boolean value to explicitly turn this on
    // an off. When this option has the value "never", reserved words
    // and keywords can also not be used as property names.
    allowReserved: null,
    // When enabled, a return at the top level is not considered an
    // error.
    allowReturnOutsideFunction: false,
    // When enabled, import/export statements are not constrained to
    // appearing at the top of the program, and an import.meta expression
    // in a script isn't considered an error.
    allowImportExportEverywhere: false,
    // By default, await identifiers are allowed to appear at the top-level scope only if ecmaVersion >= 2022.
    // When enabled, await identifiers are allowed to appear at the top-level scope,
    // but they are still not allowed in non-async functions.
    allowAwaitOutsideFunction: null,
    // When enabled, super identifiers are not constrained to
    // appearing in methods and do not raise an error when they appear elsewhere.
    allowSuperOutsideMethod: null,
    // When enabled, hashbang directive in the beginning of file is
    // allowed and treated as a line comment. Enabled by default when
    // `ecmaVersion` >= 2023.
    allowHashBang: false,
    // By default, the parser will verify that private properties are
    // only used in places where they are valid and have been declared.
    // Set this to false to turn such checks off.
    checkPrivateFields: true,
    // When `locations` is on, `loc` properties holding objects with
    // `start` and `end` properties in `{line, column}` form (with
    // line being 1-based and column 0-based) will be attached to the
    // nodes.
    locations: false,
    // A function can be passed as `onToken` option, which will
    // cause Acorn to call that function with object in the same
    // format as tokens returned from `tokenizer().getToken()`. Note
    // that you are not allowed to call the parser from the
    // callback—that will corrupt its internal state.
    onToken: null,
    // A function can be passed as `onComment` option, which will
    // cause Acorn to call that function with `(block, text, start,
    // end)` parameters whenever a comment is skipped. `block` is a
    // boolean indicating whether this is a block (`/* */`) comment,
    // `text` is the content of the comment, and `start` and `end` are
    // character offsets that denote the start and end of the comment.
    // When the `locations` option is on, two more parameters are
    // passed, the full `{line, column}` locations of the start and
    // end of the comments. Note that you are not allowed to call the
    // parser from the callback—that will corrupt its internal state.
    // When this option has an array as value, objects representing the
    // comments are pushed to it.
    onComment: null,
    // Nodes have their start and end characters offsets recorded in
    // `start` and `end` properties (directly on the node, rather than
    // the `loc` object, which holds line/column data. To also add a
    // [semi-standardized][range] `range` property holding a `[start,
    // end]` array with the same numbers, set the `ranges` option to
    // `true`.
    //
    // [range]: https://bugzilla.mozilla.org/show_bug.cgi?id=745678
    ranges: false,
    // It is possible to parse multiple files into a single AST by
    // passing the tree produced by parsing the first file as
    // `program` option in subsequent parses. This will add the
    // toplevel forms of the parsed file to the `Program` (top) node
    // of an existing parse tree.
    program: null,
    // When `locations` is on, you can pass this to record the source
    // file in every node's `loc` object.
    sourceFile: null,
    // This value, if given, is stored in every node, whether
    // `locations` is on or off.
    directSourceFile: null,
    // When enabled, parenthesized expressions are represented by
    // (non-standard) ParenthesizedExpression nodes
    preserveParens: false
  };

  // Interpret and default an options object

  var warnedAboutEcmaVersion = false;

  function getOptions(opts) {
    var options = {};

    for (var opt in defaultOptions)
      { options[opt] = opts && hasOwn(opts, opt) ? opts[opt] : defaultOptions[opt]; }

    if (options.ecmaVersion === "latest") {
      options.ecmaVersion = 1e8;
    } else if (options.ecmaVersion == null) {
      if (!warnedAboutEcmaVersion && typeof console === "object" && console.warn) {
        warnedAboutEcmaVersion = true;
        console.warn("Since Acorn 8.0.0, options.ecmaVersion is required.\nDefaulting to 2020, but this will stop working in the future.");
      }
      options.ecmaVersion = 11;
    } else if (options.ecmaVersion >= 2015) {
      options.ecmaVersion -= 2009;
    }

    if (options.allowReserved == null)
      { options.allowReserved = options.ecmaVersion < 5; }

    if (!opts || opts.allowHashBang == null)
      { options.allowHashBang = options.ecmaVersion >= 14; }

    if (isArray(options.onToken)) {
      var tokens = options.onToken;
      options.onToken = function (token) { return tokens.push(token); };
    }
    if (isArray(options.onComment))
      { options.onComment = pushComment(options, options.onComment); }

    return options
  }

  function pushComment(options, array) {
    return function(block, text, start, end, startLoc, endLoc) {
      var comment = {
        type: block ? "Block" : "Line",
        value: text,
        start: start,
        end: end
      };
      if (options.locations)
        { comment.loc = new SourceLocation(this, startLoc, endLoc); }
      if (options.ranges)
        { comment.range = [start, end]; }
      array.push(comment);
    }
  }

  // Each scope gets a bitset that may contain these flags
  var
      SCOPE_TOP = 1,
      SCOPE_FUNCTION = 2,
      SCOPE_ASYNC = 4,
      SCOPE_GENERATOR = 8,
      SCOPE_ARROW = 16,
      SCOPE_SIMPLE_CATCH = 32,
      SCOPE_SUPER = 64,
      SCOPE_DIRECT_SUPER = 128,
      SCOPE_CLASS_STATIC_BLOCK = 256,
      SCOPE_CLASS_FIELD_INIT = 512,
      SCOPE_VAR = SCOPE_TOP | SCOPE_FUNCTION | SCOPE_CLASS_STATIC_BLOCK;

  function functionFlags(async, generator) {
    return SCOPE_FUNCTION | (async ? SCOPE_ASYNC : 0) | (generator ? SCOPE_GENERATOR : 0)
  }

  // Used in checkLVal* and declareName to determine the type of a binding
  var
      BIND_NONE = 0, // Not a binding
      BIND_VAR = 1, // Var-style binding
      BIND_LEXICAL = 2, // Let- or const-style binding
      BIND_FUNCTION = 3, // Function declaration
      BIND_SIMPLE_CATCH = 4, // Simple (identifier pattern) catch binding
      BIND_OUTSIDE = 5; // Special case for function names as bound inside the function

  var Parser = function Parser(options, input, startPos) {
    this.options = options = getOptions(options);
    this.sourceFile = options.sourceFile;
    this.keywords = wordsRegexp(keywords$1[options.ecmaVersion >= 6 ? 6 : options.sourceType === "module" ? "5module" : 5]);
    var reserved = "";
    if (options.allowReserved !== true) {
      reserved = reservedWords[options.ecmaVersion >= 6 ? 6 : options.ecmaVersion === 5 ? 5 : 3];
      if (options.sourceType === "module") { reserved += " await"; }
    }
    this.reservedWords = wordsRegexp(reserved);
    var reservedStrict = (reserved ? reserved + " " : "") + reservedWords.strict;
    this.reservedWordsStrict = wordsRegexp(reservedStrict);
    this.reservedWordsStrictBind = wordsRegexp(reservedStrict + " " + reservedWords.strictBind);
    this.input = String(input);

    // Used to signal to callers of `readWord1` whether the word
    // contained any escape sequences. This is needed because words with
    // escape sequences must not be interpreted as keywords.
    this.containsEsc = false;

    // Set up token state

    // The current position of the tokenizer in the input.
    if (startPos) {
      this.pos = startPos;
      this.lineStart = this.input.lastIndexOf("\n", startPos - 1) + 1;
      this.curLine = this.input.slice(0, this.lineStart).split(lineBreak).length;
    } else {
      this.pos = this.lineStart = 0;
      this.curLine = 1;
    }

    // Properties of the current token:
    // Its type
    this.type = types$1.eof;
    // For tokens that include more information than their type, the value
    this.value = null;
    // Its start and end offset
    this.start = this.end = this.pos;
    // And, if locations are used, the {line, column} object
    // corresponding to those offsets
    this.startLoc = this.endLoc = this.curPosition();

    // Position information for the previous token
    this.lastTokEndLoc = this.lastTokStartLoc = null;
    this.lastTokStart = this.lastTokEnd = this.pos;

    // The context stack is used to superficially track syntactic
    // context to predict whether a regular expression is allowed in a
    // given position.
    this.context = this.initialContext();
    this.exprAllowed = true;

    // Figure out if it's a module code.
    this.inModule = options.sourceType === "module";
    this.strict = this.inModule || this.strictDirective(this.pos);

    // Used to signify the start of a potential arrow function
    this.potentialArrowAt = -1;
    this.potentialArrowInForAwait = false;

    // Positions to delayed-check that yield/await does not exist in default parameters.
    this.yieldPos = this.awaitPos = this.awaitIdentPos = 0;
    // Labels in scope.
    this.labels = [];
    // Thus-far undefined exports.
    this.undefinedExports = Object.create(null);

    // If enabled, skip leading hashbang line.
    if (this.pos === 0 && options.allowHashBang && this.input.slice(0, 2) === "#!")
      { this.skipLineComment(2); }

    // Scope tracking for duplicate variable names (see scope.js)
    this.scopeStack = [];
    this.enterScope(SCOPE_TOP);

    // For RegExp validation
    this.regexpState = null;

    // The stack of private names.
    // Each element has two properties: 'declared' and 'used'.
    // When it exited from the outermost class definition, all used private names must be declared.
    this.privateNameStack = [];
  };

  var prototypeAccessors = { inFunction: { configurable: true },inGenerator: { configurable: true },inAsync: { configurable: true },canAwait: { configurable: true },allowSuper: { configurable: true },allowDirectSuper: { configurable: true },treatFunctionsAsVar: { configurable: true },allowNewDotTarget: { configurable: true },inClassStaticBlock: { configurable: true } };

  Parser.prototype.parse = function parse () {
    var node = this.options.program || this.startNode();
    this.nextToken();
    return this.parseTopLevel(node)
  };

  prototypeAccessors.inFunction.get = function () { return (this.currentVarScope().flags & SCOPE_FUNCTION) > 0 };

  prototypeAccessors.inGenerator.get = function () { return (this.currentVarScope().flags & SCOPE_GENERATOR) > 0 };

  prototypeAccessors.inAsync.get = function () { return (this.currentVarScope().flags & SCOPE_ASYNC) > 0 };

  prototypeAccessors.canAwait.get = function () {
    for (var i = this.scopeStack.length - 1; i >= 0; i--) {
      var ref = this.scopeStack[i];
        var flags = ref.flags;
      if (flags & (SCOPE_CLASS_STATIC_BLOCK | SCOPE_CLASS_FIELD_INIT)) { return false }
      if (flags & SCOPE_FUNCTION) { return (flags & SCOPE_ASYNC) > 0 }
    }
    return (this.inModule && this.options.ecmaVersion >= 13) || this.options.allowAwaitOutsideFunction
  };

  prototypeAccessors.allowSuper.get = function () {
    var ref = this.currentThisScope();
      var flags = ref.flags;
    return (flags & SCOPE_SUPER) > 0 || this.options.allowSuperOutsideMethod
  };

  prototypeAccessors.allowDirectSuper.get = function () { return (this.currentThisScope().flags & SCOPE_DIRECT_SUPER) > 0 };

  prototypeAccessors.treatFunctionsAsVar.get = function () { return this.treatFunctionsAsVarInScope(this.currentScope()) };

  prototypeAccessors.allowNewDotTarget.get = function () {
    for (var i = this.scopeStack.length - 1; i >= 0; i--) {
      var ref = this.scopeStack[i];
        var flags = ref.flags;
      if (flags & (SCOPE_CLASS_STATIC_BLOCK | SCOPE_CLASS_FIELD_INIT) ||
          ((flags & SCOPE_FUNCTION) && !(flags & SCOPE_ARROW))) { return true }
    }
    return false
  };

  prototypeAccessors.inClassStaticBlock.get = function () {
    return (this.currentVarScope().flags & SCOPE_CLASS_STATIC_BLOCK) > 0
  };

  Parser.extend = function extend () {
      var plugins = [], len = arguments.length;
      while ( len-- ) plugins[ len ] = arguments[ len ];

    var cls = this;
    for (var i = 0; i < plugins.length; i++) { cls = plugins[i](cls); }
    return cls
  };

  Parser.parse = function parse (input, options) {
    return new this(options, input).parse()
  };

  Parser.parseExpressionAt = function parseExpressionAt (input, pos, options) {
    var parser = new this(options, input, pos);
    parser.nextToken();
    return parser.parseExpression()
  };

  Parser.tokenizer = function tokenizer (input, options) {
    return new this(options, input)
  };

  Object.defineProperties( Parser.prototype, prototypeAccessors );

  var pp$9 = Parser.prototype;

  // ## Parser utilities

  var literal = /^(?:'((?:\\[^]|[^'\\])*?)'|"((?:\\[^]|[^"\\])*?)")/;
  pp$9.strictDirective = function(start) {
    if (this.options.ecmaVersion < 5) { return false }
    for (;;) {
      // Try to find string literal.
      skipWhiteSpace.lastIndex = start;
      start += skipWhiteSpace.exec(this.input)[0].length;
      var match = literal.exec(this.input.slice(start));
      if (!match) { return false }
      if ((match[1] || match[2]) === "use strict") {
        skipWhiteSpace.lastIndex = start + match[0].length;
        var spaceAfter = skipWhiteSpace.exec(this.input), end = spaceAfter.index + spaceAfter[0].length;
        var next = this.input.charAt(end);
        return next === ";" || next === "}" ||
          (lineBreak.test(spaceAfter[0]) &&
           !(/[(`.[+\-/*%<>=,?^&]/.test(next) || next === "!" && this.input.charAt(end + 1) === "="))
      }
      start += match[0].length;

      // Skip semicolon, if any.
      skipWhiteSpace.lastIndex = start;
      start += skipWhiteSpace.exec(this.input)[0].length;
      if (this.input[start] === ";")
        { start++; }
    }
  };

  // Predicate that tests whether the next token is of the given
  // type, and if yes, consumes it as a side effect.

  pp$9.eat = function(type) {
    if (this.type === type) {
      this.next();
      return true
    } else {
      return false
    }
  };

  // Tests whether parsed token is a contextual keyword.

  pp$9.isContextual = function(name) {
    return this.type === types$1.name && this.value === name && !this.containsEsc
  };

  // Consumes contextual keyword if possible.

  pp$9.eatContextual = function(name) {
    if (!this.isContextual(name)) { return false }
    this.next();
    return true
  };

  // Asserts that following token is given contextual keyword.

  pp$9.expectContextual = function(name) {
    if (!this.eatContextual(name)) { this.unexpected(); }
  };

  // Test whether a semicolon can be inserted at the current position.

  pp$9.canInsertSemicolon = function() {
    return this.type === types$1.eof ||
      this.type === types$1.braceR ||
      lineBreak.test(this.input.slice(this.lastTokEnd, this.start))
  };

  pp$9.insertSemicolon = function() {
    if (this.canInsertSemicolon()) {
      if (this.options.onInsertedSemicolon)
        { this.options.onInsertedSemicolon(this.lastTokEnd, this.lastTokEndLoc); }
      return true
    }
  };

  // Consume a semicolon, or, failing that, see if we are allowed to
  // pretend that there is a semicolon at this position.

  pp$9.semicolon = function() {
    if (!this.eat(types$1.semi) && !this.insertSemicolon()) { this.unexpected(); }
  };

  pp$9.afterTrailingComma = function(tokType, notNext) {
    if (this.type === tokType) {
      if (this.options.onTrailingComma)
        { this.options.onTrailingComma(this.lastTokStart, this.lastTokStartLoc); }
      if (!notNext)
        { this.next(); }
      return true
    }
  };

  // Expect a token of a given type. If found, consume it, otherwise,
  // raise an unexpected token error.

  pp$9.expect = function(type) {
    this.eat(type) || this.unexpected();
  };

  // Raise an unexpected token error.

  pp$9.unexpected = function(pos) {
    this.raise(pos != null ? pos : this.start, "Unexpected token");
  };

  var DestructuringErrors = function DestructuringErrors() {
    this.shorthandAssign =
    this.trailingComma =
    this.parenthesizedAssign =
    this.parenthesizedBind =
    this.doubleProto =
      -1;
  };

  pp$9.checkPatternErrors = function(refDestructuringErrors, isAssign) {
    if (!refDestructuringErrors) { return }
    if (refDestructuringErrors.trailingComma > -1)
      { this.raiseRecoverable(refDestructuringErrors.trailingComma, "Comma is not permitted after the rest element"); }
    var parens = isAssign ? refDestructuringErrors.parenthesizedAssign : refDestructuringErrors.parenthesizedBind;
    if (parens > -1) { this.raiseRecoverable(parens, isAssign ? "Assigning to rvalue" : "Parenthesized pattern"); }
  };

  pp$9.checkExpressionErrors = function(refDestructuringErrors, andThrow) {
    if (!refDestructuringErrors) { return false }
    var shorthandAssign = refDestructuringErrors.shorthandAssign;
    var doubleProto = refDestructuringErrors.doubleProto;
    if (!andThrow) { return shorthandAssign >= 0 || doubleProto >= 0 }
    if (shorthandAssign >= 0)
      { this.raise(shorthandAssign, "Shorthand property assignments are valid only in destructuring patterns"); }
    if (doubleProto >= 0)
      { this.raiseRecoverable(doubleProto, "Redefinition of __proto__ property"); }
  };

  pp$9.checkYieldAwaitInDefaultParams = function() {
    if (this.yieldPos && (!this.awaitPos || this.yieldPos < this.awaitPos))
      { this.raise(this.yieldPos, "Yield expression cannot be a default value"); }
    if (this.awaitPos)
      { this.raise(this.awaitPos, "Await expression cannot be a default value"); }
  };

  pp$9.isSimpleAssignTarget = function(expr) {
    if (expr.type === "ParenthesizedExpression")
      { return this.isSimpleAssignTarget(expr.expression) }
    return expr.type === "Identifier" || expr.type === "MemberExpression"
  };

  var pp$8 = Parser.prototype;

  // ### Statement parsing

  // Parse a program. Initializes the parser, reads any number of
  // statements, and wraps them in a Program node.  Optionally takes a
  // `program` argument.  If present, the statements will be appended
  // to its body instead of creating a new node.

  pp$8.parseTopLevel = function(node) {
    var exports$1 = Object.create(null);
    if (!node.body) { node.body = []; }
    while (this.type !== types$1.eof) {
      var stmt = this.parseStatement(null, true, exports$1);
      node.body.push(stmt);
    }
    if (this.inModule)
      { for (var i = 0, list = Object.keys(this.undefinedExports); i < list.length; i += 1)
        {
          var name = list[i];

          this.raiseRecoverable(this.undefinedExports[name].start, ("Export '" + name + "' is not defined"));
        } }
    this.adaptDirectivePrologue(node.body);
    this.next();
    node.sourceType = this.options.sourceType;
    return this.finishNode(node, "Program")
  };

  var loopLabel = {kind: "loop"}, switchLabel = {kind: "switch"};

  pp$8.isLet = function(context) {
    if (this.options.ecmaVersion < 6 || !this.isContextual("let")) { return false }
    skipWhiteSpace.lastIndex = this.pos;
    var skip = skipWhiteSpace.exec(this.input);
    var next = this.pos + skip[0].length, nextCh = this.input.charCodeAt(next);
    // For ambiguous cases, determine if a LexicalDeclaration (or only a
    // Statement) is allowed here. If context is not empty then only a Statement
    // is allowed. However, `let [` is an explicit negative lookahead for
    // ExpressionStatement, so special-case it first.
    if (nextCh === 91 || nextCh === 92) { return true } // '[', '\'
    if (context) { return false }

    if (nextCh === 123 || nextCh > 0xd7ff && nextCh < 0xdc00) { return true } // '{', astral
    if (isIdentifierStart(nextCh, true)) {
      var pos = next + 1;
      while (isIdentifierChar(nextCh = this.input.charCodeAt(pos), true)) { ++pos; }
      if (nextCh === 92 || nextCh > 0xd7ff && nextCh < 0xdc00) { return true }
      var ident = this.input.slice(next, pos);
      if (!keywordRelationalOperator.test(ident)) { return true }
    }
    return false
  };

  // check 'async [no LineTerminator here] function'
  // - 'async /*foo*/ function' is OK.
  // - 'async /*\n*/ function' is invalid.
  pp$8.isAsyncFunction = function() {
    if (this.options.ecmaVersion < 8 || !this.isContextual("async"))
      { return false }

    skipWhiteSpace.lastIndex = this.pos;
    var skip = skipWhiteSpace.exec(this.input);
    var next = this.pos + skip[0].length, after;
    return !lineBreak.test(this.input.slice(this.pos, next)) &&
      this.input.slice(next, next + 8) === "function" &&
      (next + 8 === this.input.length ||
       !(isIdentifierChar(after = this.input.charCodeAt(next + 8)) || after > 0xd7ff && after < 0xdc00))
  };

  pp$8.isUsingKeyword = function(isAwaitUsing, isFor) {
    if (this.options.ecmaVersion < 17 || !this.isContextual(isAwaitUsing ? "await" : "using"))
      { return false }

    skipWhiteSpace.lastIndex = this.pos;
    var skip = skipWhiteSpace.exec(this.input);
    var next = this.pos + skip[0].length;

    if (lineBreak.test(this.input.slice(this.pos, next))) { return false }

    if (isAwaitUsing) {
      var awaitEndPos = next + 5 /* await */, after;
      if (this.input.slice(next, awaitEndPos) !== "using" ||
        awaitEndPos === this.input.length ||
        isIdentifierChar(after = this.input.charCodeAt(awaitEndPos)) ||
        (after > 0xd7ff && after < 0xdc00)
      ) { return false }

      skipWhiteSpace.lastIndex = awaitEndPos;
      var skipAfterUsing = skipWhiteSpace.exec(this.input);
      if (skipAfterUsing && lineBreak.test(this.input.slice(awaitEndPos, awaitEndPos + skipAfterUsing[0].length))) { return false }
    }

    if (isFor) {
      var ofEndPos = next + 2 /* of */, after$1;
      if (this.input.slice(next, ofEndPos) === "of") {
        if (ofEndPos === this.input.length ||
          (!isIdentifierChar(after$1 = this.input.charCodeAt(ofEndPos)) && !(after$1 > 0xd7ff && after$1 < 0xdc00))) { return false }
      }
    }

    var ch = this.input.charCodeAt(next);
    return isIdentifierStart(ch, true) || ch === 92 // '\'
  };

  pp$8.isAwaitUsing = function(isFor) {
    return this.isUsingKeyword(true, isFor)
  };

  pp$8.isUsing = function(isFor) {
    return this.isUsingKeyword(false, isFor)
  };

  // Parse a single statement.
  //
  // If expecting a statement and finding a slash operator, parse a
  // regular expression literal. This is to handle cases like
  // `if (foo) /blah/.exec(foo)`, where looking at the previous token
  // does not help.

  pp$8.parseStatement = function(context, topLevel, exports$1) {
    var starttype = this.type, node = this.startNode(), kind;

    if (this.isLet(context)) {
      starttype = types$1._var;
      kind = "let";
    }

    // Most types of statements are recognized by the keyword they
    // start with. Many are trivial to parse, some require a bit of
    // complexity.

    switch (starttype) {
    case types$1._break: case types$1._continue: return this.parseBreakContinueStatement(node, starttype.keyword)
    case types$1._debugger: return this.parseDebuggerStatement(node)
    case types$1._do: return this.parseDoStatement(node)
    case types$1._for: return this.parseForStatement(node)
    case types$1._function:
      // Function as sole body of either an if statement or a labeled statement
      // works, but not when it is part of a labeled statement that is the sole
      // body of an if statement.
      if ((context && (this.strict || context !== "if" && context !== "label")) && this.options.ecmaVersion >= 6) { this.unexpected(); }
      return this.parseFunctionStatement(node, false, !context)
    case types$1._class:
      if (context) { this.unexpected(); }
      return this.parseClass(node, true)
    case types$1._if: return this.parseIfStatement(node)
    case types$1._return: return this.parseReturnStatement(node)
    case types$1._switch: return this.parseSwitchStatement(node)
    case types$1._throw: return this.parseThrowStatement(node)
    case types$1._try: return this.parseTryStatement(node)
    case types$1._const: case types$1._var:
      kind = kind || this.value;
      if (context && kind !== "var") { this.unexpected(); }
      return this.parseVarStatement(node, kind)
    case types$1._while: return this.parseWhileStatement(node)
    case types$1._with: return this.parseWithStatement(node)
    case types$1.braceL: return this.parseBlock(true, node)
    case types$1.semi: return this.parseEmptyStatement(node)
    case types$1._export:
    case types$1._import:
      if (this.options.ecmaVersion > 10 && starttype === types$1._import) {
        skipWhiteSpace.lastIndex = this.pos;
        var skip = skipWhiteSpace.exec(this.input);
        var next = this.pos + skip[0].length, nextCh = this.input.charCodeAt(next);
        if (nextCh === 40 || nextCh === 46) // '(' or '.'
          { return this.parseExpressionStatement(node, this.parseExpression()) }
      }

      if (!this.options.allowImportExportEverywhere) {
        if (!topLevel)
          { this.raise(this.start, "'import' and 'export' may only appear at the top level"); }
        if (!this.inModule)
          { this.raise(this.start, "'import' and 'export' may appear only with 'sourceType: module'"); }
      }
      return starttype === types$1._import ? this.parseImport(node) : this.parseExport(node, exports$1)

      // If the statement does not start with a statement keyword or a
      // brace, it's an ExpressionStatement or LabeledStatement. We
      // simply start parsing an expression, and afterwards, if the
      // next token is a colon and the expression was a simple
      // Identifier node, we switch to interpreting it as a label.
    default:
      if (this.isAsyncFunction()) {
        if (context) { this.unexpected(); }
        this.next();
        return this.parseFunctionStatement(node, true, !context)
      }

      var usingKind = this.isAwaitUsing(false) ? "await using" : this.isUsing(false) ? "using" : null;
      if (usingKind) {
        if (topLevel && this.options.sourceType === "script") {
          this.raise(this.start, "Using declaration cannot appear in the top level when source type is `script`");
        }
        if (usingKind === "await using") {
          if (!this.canAwait) {
            this.raise(this.start, "Await using cannot appear outside of async function");
          }
          this.next();
        }
        this.next();
        this.parseVar(node, false, usingKind);
        this.semicolon();
        return this.finishNode(node, "VariableDeclaration")
      }

      var maybeName = this.value, expr = this.parseExpression();
      if (starttype === types$1.name && expr.type === "Identifier" && this.eat(types$1.colon))
        { return this.parseLabeledStatement(node, maybeName, expr, context) }
      else { return this.parseExpressionStatement(node, expr) }
    }
  };

  pp$8.parseBreakContinueStatement = function(node, keyword) {
    var isBreak = keyword === "break";
    this.next();
    if (this.eat(types$1.semi) || this.insertSemicolon()) { node.label = null; }
    else if (this.type !== types$1.name) { this.unexpected(); }
    else {
      node.label = this.parseIdent();
      this.semicolon();
    }

    // Verify that there is an actual destination to break or
    // continue to.
    var i = 0;
    for (; i < this.labels.length; ++i) {
      var lab = this.labels[i];
      if (node.label == null || lab.name === node.label.name) {
        if (lab.kind != null && (isBreak || lab.kind === "loop")) { break }
        if (node.label && isBreak) { break }
      }
    }
    if (i === this.labels.length) { this.raise(node.start, "Unsyntactic " + keyword); }
    return this.finishNode(node, isBreak ? "BreakStatement" : "ContinueStatement")
  };

  pp$8.parseDebuggerStatement = function(node) {
    this.next();
    this.semicolon();
    return this.finishNode(node, "DebuggerStatement")
  };

  pp$8.parseDoStatement = function(node) {
    this.next();
    this.labels.push(loopLabel);
    node.body = this.parseStatement("do");
    this.labels.pop();
    this.expect(types$1._while);
    node.test = this.parseParenExpression();
    if (this.options.ecmaVersion >= 6)
      { this.eat(types$1.semi); }
    else
      { this.semicolon(); }
    return this.finishNode(node, "DoWhileStatement")
  };

  // Disambiguating between a `for` and a `for`/`in` or `for`/`of`
  // loop is non-trivial. Basically, we have to parse the init `var`
  // statement or expression, disallowing the `in` operator (see
  // the second parameter to `parseExpression`), and then check
  // whether the next token is `in` or `of`. When there is no init
  // part (semicolon immediately after the opening parenthesis), it
  // is a regular `for` loop.

  pp$8.parseForStatement = function(node) {
    this.next();
    var awaitAt = (this.options.ecmaVersion >= 9 && this.canAwait && this.eatContextual("await")) ? this.lastTokStart : -1;
    this.labels.push(loopLabel);
    this.enterScope(0);
    this.expect(types$1.parenL);
    if (this.type === types$1.semi) {
      if (awaitAt > -1) { this.unexpected(awaitAt); }
      return this.parseFor(node, null)
    }
    var isLet = this.isLet();
    if (this.type === types$1._var || this.type === types$1._const || isLet) {
      var init$1 = this.startNode(), kind = isLet ? "let" : this.value;
      this.next();
      this.parseVar(init$1, true, kind);
      this.finishNode(init$1, "VariableDeclaration");
      return this.parseForAfterInit(node, init$1, awaitAt)
    }
    var startsWithLet = this.isContextual("let"), isForOf = false;

    var usingKind = this.isUsing(true) ? "using" : this.isAwaitUsing(true) ? "await using" : null;
    if (usingKind) {
      var init$2 = this.startNode();
      this.next();
      if (usingKind === "await using") { this.next(); }
      this.parseVar(init$2, true, usingKind);
      this.finishNode(init$2, "VariableDeclaration");
      return this.parseForAfterInit(node, init$2, awaitAt)
    }
    var containsEsc = this.containsEsc;
    var refDestructuringErrors = new DestructuringErrors;
    var initPos = this.start;
    var init = awaitAt > -1
      ? this.parseExprSubscripts(refDestructuringErrors, "await")
      : this.parseExpression(true, refDestructuringErrors);
    if (this.type === types$1._in || (isForOf = this.options.ecmaVersion >= 6 && this.isContextual("of"))) {
      if (awaitAt > -1) { // implies `ecmaVersion >= 9` (see declaration of awaitAt)
        if (this.type === types$1._in) { this.unexpected(awaitAt); }
        node.await = true;
      } else if (isForOf && this.options.ecmaVersion >= 8) {
        if (init.start === initPos && !containsEsc && init.type === "Identifier" && init.name === "async") { this.unexpected(); }
        else if (this.options.ecmaVersion >= 9) { node.await = false; }
      }
      if (startsWithLet && isForOf) { this.raise(init.start, "The left-hand side of a for-of loop may not start with 'let'."); }
      this.toAssignable(init, false, refDestructuringErrors);
      this.checkLValPattern(init);
      return this.parseForIn(node, init)
    } else {
      this.checkExpressionErrors(refDestructuringErrors, true);
    }
    if (awaitAt > -1) { this.unexpected(awaitAt); }
    return this.parseFor(node, init)
  };

  // Helper method to parse for loop after variable initialization
  pp$8.parseForAfterInit = function(node, init, awaitAt) {
    if ((this.type === types$1._in || (this.options.ecmaVersion >= 6 && this.isContextual("of"))) && init.declarations.length === 1) {
      if (this.options.ecmaVersion >= 9) {
        if (this.type === types$1._in) {
          if (awaitAt > -1) { this.unexpected(awaitAt); }
        } else { node.await = awaitAt > -1; }
      }
      return this.parseForIn(node, init)
    }
    if (awaitAt > -1) { this.unexpected(awaitAt); }
    return this.parseFor(node, init)
  };

  pp$8.parseFunctionStatement = function(node, isAsync, declarationPosition) {
    this.next();
    return this.parseFunction(node, FUNC_STATEMENT | (declarationPosition ? 0 : FUNC_HANGING_STATEMENT), false, isAsync)
  };

  pp$8.parseIfStatement = function(node) {
    this.next();
    node.test = this.parseParenExpression();
    // allow function declarations in branches, but only in non-strict mode
    node.consequent = this.parseStatement("if");
    node.alternate = this.eat(types$1._else) ? this.parseStatement("if") : null;
    return this.finishNode(node, "IfStatement")
  };

  pp$8.parseReturnStatement = function(node) {
    if (!this.inFunction && !this.options.allowReturnOutsideFunction)
      { this.raise(this.start, "'return' outside of function"); }
    this.next();

    // In `return` (and `break`/`continue`), the keywords with
    // optional arguments, we eagerly look for a semicolon or the
    // possibility to insert one.

    if (this.eat(types$1.semi) || this.insertSemicolon()) { node.argument = null; }
    else { node.argument = this.parseExpression(); this.semicolon(); }
    return this.finishNode(node, "ReturnStatement")
  };

  pp$8.parseSwitchStatement = function(node) {
    this.next();
    node.discriminant = this.parseParenExpression();
    node.cases = [];
    this.expect(types$1.braceL);
    this.labels.push(switchLabel);
    this.enterScope(0);

    // Statements under must be grouped (by label) in SwitchCase
    // nodes. `cur` is used to keep the node that we are currently
    // adding statements to.

    var cur;
    for (var sawDefault = false; this.type !== types$1.braceR;) {
      if (this.type === types$1._case || this.type === types$1._default) {
        var isCase = this.type === types$1._case;
        if (cur) { this.finishNode(cur, "SwitchCase"); }
        node.cases.push(cur = this.startNode());
        cur.consequent = [];
        this.next();
        if (isCase) {
          cur.test = this.parseExpression();
        } else {
          if (sawDefault) { this.raiseRecoverable(this.lastTokStart, "Multiple default clauses"); }
          sawDefault = true;
          cur.test = null;
        }
        this.expect(types$1.colon);
      } else {
        if (!cur) { this.unexpected(); }
        cur.consequent.push(this.parseStatement(null));
      }
    }
    this.exitScope();
    if (cur) { this.finishNode(cur, "SwitchCase"); }
    this.next(); // Closing brace
    this.labels.pop();
    return this.finishNode(node, "SwitchStatement")
  };

  pp$8.parseThrowStatement = function(node) {
    this.next();
    if (lineBreak.test(this.input.slice(this.lastTokEnd, this.start)))
      { this.raise(this.lastTokEnd, "Illegal newline after throw"); }
    node.argument = this.parseExpression();
    this.semicolon();
    return this.finishNode(node, "ThrowStatement")
  };

  // Reused empty array added for node fields that are always empty.

  var empty$1 = [];

  pp$8.parseCatchClauseParam = function() {
    var param = this.parseBindingAtom();
    var simple = param.type === "Identifier";
    this.enterScope(simple ? SCOPE_SIMPLE_CATCH : 0);
    this.checkLValPattern(param, simple ? BIND_SIMPLE_CATCH : BIND_LEXICAL);
    this.expect(types$1.parenR);

    return param
  };

  pp$8.parseTryStatement = function(node) {
    this.next();
    node.block = this.parseBlock();
    node.handler = null;
    if (this.type === types$1._catch) {
      var clause = this.startNode();
      this.next();
      if (this.eat(types$1.parenL)) {
        clause.param = this.parseCatchClauseParam();
      } else {
        if (this.options.ecmaVersion < 10) { this.unexpected(); }
        clause.param = null;
        this.enterScope(0);
      }
      clause.body = this.parseBlock(false);
      this.exitScope();
      node.handler = this.finishNode(clause, "CatchClause");
    }
    node.finalizer = this.eat(types$1._finally) ? this.parseBlock() : null;
    if (!node.handler && !node.finalizer)
      { this.raise(node.start, "Missing catch or finally clause"); }
    return this.finishNode(node, "TryStatement")
  };

  pp$8.parseVarStatement = function(node, kind, allowMissingInitializer) {
    this.next();
    this.parseVar(node, false, kind, allowMissingInitializer);
    this.semicolon();
    return this.finishNode(node, "VariableDeclaration")
  };

  pp$8.parseWhileStatement = function(node) {
    this.next();
    node.test = this.parseParenExpression();
    this.labels.push(loopLabel);
    node.body = this.parseStatement("while");
    this.labels.pop();
    return this.finishNode(node, "WhileStatement")
  };

  pp$8.parseWithStatement = function(node) {
    if (this.strict) { this.raise(this.start, "'with' in strict mode"); }
    this.next();
    node.object = this.parseParenExpression();
    node.body = this.parseStatement("with");
    return this.finishNode(node, "WithStatement")
  };

  pp$8.parseEmptyStatement = function(node) {
    this.next();
    return this.finishNode(node, "EmptyStatement")
  };

  pp$8.parseLabeledStatement = function(node, maybeName, expr, context) {
    for (var i$1 = 0, list = this.labels; i$1 < list.length; i$1 += 1)
      {
      var label = list[i$1];

      if (label.name === maybeName)
        { this.raise(expr.start, "Label '" + maybeName + "' is already declared");
    } }
    var kind = this.type.isLoop ? "loop" : this.type === types$1._switch ? "switch" : null;
    for (var i = this.labels.length - 1; i >= 0; i--) {
      var label$1 = this.labels[i];
      if (label$1.statementStart === node.start) {
        // Update information about previous labels on this node
        label$1.statementStart = this.start;
        label$1.kind = kind;
      } else { break }
    }
    this.labels.push({name: maybeName, kind: kind, statementStart: this.start});
    node.body = this.parseStatement(context ? context.indexOf("label") === -1 ? context + "label" : context : "label");
    this.labels.pop();
    node.label = expr;
    return this.finishNode(node, "LabeledStatement")
  };

  pp$8.parseExpressionStatement = function(node, expr) {
    node.expression = expr;
    this.semicolon();
    return this.finishNode(node, "ExpressionStatement")
  };

  // Parse a semicolon-enclosed block of statements, handling `"use
  // strict"` declarations when `allowStrict` is true (used for
  // function bodies).

  pp$8.parseBlock = function(createNewLexicalScope, node, exitStrict) {
    if ( createNewLexicalScope === void 0 ) createNewLexicalScope = true;
    if ( node === void 0 ) node = this.startNode();

    node.body = [];
    this.expect(types$1.braceL);
    if (createNewLexicalScope) { this.enterScope(0); }
    while (this.type !== types$1.braceR) {
      var stmt = this.parseStatement(null);
      node.body.push(stmt);
    }
    if (exitStrict) { this.strict = false; }
    this.next();
    if (createNewLexicalScope) { this.exitScope(); }
    return this.finishNode(node, "BlockStatement")
  };

  // Parse a regular `for` loop. The disambiguation code in
  // `parseStatement` will already have parsed the init statement or
  // expression.

  pp$8.parseFor = function(node, init) {
    node.init = init;
    this.expect(types$1.semi);
    node.test = this.type === types$1.semi ? null : this.parseExpression();
    this.expect(types$1.semi);
    node.update = this.type === types$1.parenR ? null : this.parseExpression();
    this.expect(types$1.parenR);
    node.body = this.parseStatement("for");
    this.exitScope();
    this.labels.pop();
    return this.finishNode(node, "ForStatement")
  };

  // Parse a `for`/`in` and `for`/`of` loop, which are almost
  // same from parser's perspective.

  pp$8.parseForIn = function(node, init) {
    var isForIn = this.type === types$1._in;
    this.next();

    if (
      init.type === "VariableDeclaration" &&
      init.declarations[0].init != null &&
      (
        !isForIn ||
        this.options.ecmaVersion < 8 ||
        this.strict ||
        init.kind !== "var" ||
        init.declarations[0].id.type !== "Identifier"
      )
    ) {
      this.raise(
        init.start,
        ((isForIn ? "for-in" : "for-of") + " loop variable declaration may not have an initializer")
      );
    }
    node.left = init;
    node.right = isForIn ? this.parseExpression() : this.parseMaybeAssign();
    this.expect(types$1.parenR);
    node.body = this.parseStatement("for");
    this.exitScope();
    this.labels.pop();
    return this.finishNode(node, isForIn ? "ForInStatement" : "ForOfStatement")
  };

  // Parse a list of variable declarations.

  pp$8.parseVar = function(node, isFor, kind, allowMissingInitializer) {
    node.declarations = [];
    node.kind = kind;
    for (;;) {
      var decl = this.startNode();
      this.parseVarId(decl, kind);
      if (this.eat(types$1.eq)) {
        decl.init = this.parseMaybeAssign(isFor);
      } else if (!allowMissingInitializer && kind === "const" && !(this.type === types$1._in || (this.options.ecmaVersion >= 6 && this.isContextual("of")))) {
        this.unexpected();
      } else if (!allowMissingInitializer && (kind === "using" || kind === "await using") && this.options.ecmaVersion >= 17 && this.type !== types$1._in && !this.isContextual("of")) {
        this.raise(this.lastTokEnd, ("Missing initializer in " + kind + " declaration"));
      } else if (!allowMissingInitializer && decl.id.type !== "Identifier" && !(isFor && (this.type === types$1._in || this.isContextual("of")))) {
        this.raise(this.lastTokEnd, "Complex binding patterns require an initialization value");
      } else {
        decl.init = null;
      }
      node.declarations.push(this.finishNode(decl, "VariableDeclarator"));
      if (!this.eat(types$1.comma)) { break }
    }
    return node
  };

  pp$8.parseVarId = function(decl, kind) {
    decl.id = kind === "using" || kind === "await using"
      ? this.parseIdent()
      : this.parseBindingAtom();

    this.checkLValPattern(decl.id, kind === "var" ? BIND_VAR : BIND_LEXICAL, false);
  };

  var FUNC_STATEMENT = 1, FUNC_HANGING_STATEMENT = 2, FUNC_NULLABLE_ID = 4;

  // Parse a function declaration or literal (depending on the
  // `statement & FUNC_STATEMENT`).

  // Remove `allowExpressionBody` for 7.0.0, as it is only called with false
  pp$8.parseFunction = function(node, statement, allowExpressionBody, isAsync, forInit) {
    this.initFunction(node);
    if (this.options.ecmaVersion >= 9 || this.options.ecmaVersion >= 6 && !isAsync) {
      if (this.type === types$1.star && (statement & FUNC_HANGING_STATEMENT))
        { this.unexpected(); }
      node.generator = this.eat(types$1.star);
    }
    if (this.options.ecmaVersion >= 8)
      { node.async = !!isAsync; }

    if (statement & FUNC_STATEMENT) {
      node.id = (statement & FUNC_NULLABLE_ID) && this.type !== types$1.name ? null : this.parseIdent();
      if (node.id && !(statement & FUNC_HANGING_STATEMENT))
        // If it is a regular function declaration in sloppy mode, then it is
        // subject to Annex B semantics (BIND_FUNCTION). Otherwise, the binding
        // mode depends on properties of the current scope (see
        // treatFunctionsAsVar).
        { this.checkLValSimple(node.id, (this.strict || node.generator || node.async) ? this.treatFunctionsAsVar ? BIND_VAR : BIND_LEXICAL : BIND_FUNCTION); }
    }

    var oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldAwaitIdentPos = this.awaitIdentPos;
    this.yieldPos = 0;
    this.awaitPos = 0;
    this.awaitIdentPos = 0;
    this.enterScope(functionFlags(node.async, node.generator));

    if (!(statement & FUNC_STATEMENT))
      { node.id = this.type === types$1.name ? this.parseIdent() : null; }

    this.parseFunctionParams(node);
    this.parseFunctionBody(node, allowExpressionBody, false, forInit);

    this.yieldPos = oldYieldPos;
    this.awaitPos = oldAwaitPos;
    this.awaitIdentPos = oldAwaitIdentPos;
    return this.finishNode(node, (statement & FUNC_STATEMENT) ? "FunctionDeclaration" : "FunctionExpression")
  };

  pp$8.parseFunctionParams = function(node) {
    this.expect(types$1.parenL);
    node.params = this.parseBindingList(types$1.parenR, false, this.options.ecmaVersion >= 8);
    this.checkYieldAwaitInDefaultParams();
  };

  // Parse a class declaration or literal (depending on the
  // `isStatement` parameter).

  pp$8.parseClass = function(node, isStatement) {
    this.next();

    // ecma-262 14.6 Class Definitions
    // A class definition is always strict mode code.
    var oldStrict = this.strict;
    this.strict = true;

    this.parseClassId(node, isStatement);
    this.parseClassSuper(node);
    var privateNameMap = this.enterClassBody();
    var classBody = this.startNode();
    var hadConstructor = false;
    classBody.body = [];
    this.expect(types$1.braceL);
    while (this.type !== types$1.braceR) {
      var element = this.parseClassElement(node.superClass !== null);
      if (element) {
        classBody.body.push(element);
        if (element.type === "MethodDefinition" && element.kind === "constructor") {
          if (hadConstructor) { this.raiseRecoverable(element.start, "Duplicate constructor in the same class"); }
          hadConstructor = true;
        } else if (element.key && element.key.type === "PrivateIdentifier" && isPrivateNameConflicted(privateNameMap, element)) {
          this.raiseRecoverable(element.key.start, ("Identifier '#" + (element.key.name) + "' has already been declared"));
        }
      }
    }
    this.strict = oldStrict;
    this.next();
    node.body = this.finishNode(classBody, "ClassBody");
    this.exitClassBody();
    return this.finishNode(node, isStatement ? "ClassDeclaration" : "ClassExpression")
  };

  pp$8.parseClassElement = function(constructorAllowsSuper) {
    if (this.eat(types$1.semi)) { return null }

    var ecmaVersion = this.options.ecmaVersion;
    var node = this.startNode();
    var keyName = "";
    var isGenerator = false;
    var isAsync = false;
    var kind = "method";
    var isStatic = false;

    if (this.eatContextual("static")) {
      // Parse static init block
      if (ecmaVersion >= 13 && this.eat(types$1.braceL)) {
        this.parseClassStaticBlock(node);
        return node
      }
      if (this.isClassElementNameStart() || this.type === types$1.star) {
        isStatic = true;
      } else {
        keyName = "static";
      }
    }
    node.static = isStatic;
    if (!keyName && ecmaVersion >= 8 && this.eatContextual("async")) {
      if ((this.isClassElementNameStart() || this.type === types$1.star) && !this.canInsertSemicolon()) {
        isAsync = true;
      } else {
        keyName = "async";
      }
    }
    if (!keyName && (ecmaVersion >= 9 || !isAsync) && this.eat(types$1.star)) {
      isGenerator = true;
    }
    if (!keyName && !isAsync && !isGenerator) {
      var lastValue = this.value;
      if (this.eatContextual("get") || this.eatContextual("set")) {
        if (this.isClassElementNameStart()) {
          kind = lastValue;
        } else {
          keyName = lastValue;
        }
      }
    }

    // Parse element name
    if (keyName) {
      // 'async', 'get', 'set', or 'static' were not a keyword contextually.
      // The last token is any of those. Make it the element name.
      node.computed = false;
      node.key = this.startNodeAt(this.lastTokStart, this.lastTokStartLoc);
      node.key.name = keyName;
      this.finishNode(node.key, "Identifier");
    } else {
      this.parseClassElementName(node);
    }

    // Parse element value
    if (ecmaVersion < 13 || this.type === types$1.parenL || kind !== "method" || isGenerator || isAsync) {
      var isConstructor = !node.static && checkKeyName(node, "constructor");
      var allowsDirectSuper = isConstructor && constructorAllowsSuper;
      // Couldn't move this check into the 'parseClassMethod' method for backward compatibility.
      if (isConstructor && kind !== "method") { this.raise(node.key.start, "Constructor can't have get/set modifier"); }
      node.kind = isConstructor ? "constructor" : kind;
      this.parseClassMethod(node, isGenerator, isAsync, allowsDirectSuper);
    } else {
      this.parseClassField(node);
    }

    return node
  };

  pp$8.isClassElementNameStart = function() {
    return (
      this.type === types$1.name ||
      this.type === types$1.privateId ||
      this.type === types$1.num ||
      this.type === types$1.string ||
      this.type === types$1.bracketL ||
      this.type.keyword
    )
  };

  pp$8.parseClassElementName = function(element) {
    if (this.type === types$1.privateId) {
      if (this.value === "constructor") {
        this.raise(this.start, "Classes can't have an element named '#constructor'");
      }
      element.computed = false;
      element.key = this.parsePrivateIdent();
    } else {
      this.parsePropertyName(element);
    }
  };

  pp$8.parseClassMethod = function(method, isGenerator, isAsync, allowsDirectSuper) {
    // Check key and flags
    var key = method.key;
    if (method.kind === "constructor") {
      if (isGenerator) { this.raise(key.start, "Constructor can't be a generator"); }
      if (isAsync) { this.raise(key.start, "Constructor can't be an async method"); }
    } else if (method.static && checkKeyName(method, "prototype")) {
      this.raise(key.start, "Classes may not have a static property named prototype");
    }

    // Parse value
    var value = method.value = this.parseMethod(isGenerator, isAsync, allowsDirectSuper);

    // Check value
    if (method.kind === "get" && value.params.length !== 0)
      { this.raiseRecoverable(value.start, "getter should have no params"); }
    if (method.kind === "set" && value.params.length !== 1)
      { this.raiseRecoverable(value.start, "setter should have exactly one param"); }
    if (method.kind === "set" && value.params[0].type === "RestElement")
      { this.raiseRecoverable(value.params[0].start, "Setter cannot use rest params"); }

    return this.finishNode(method, "MethodDefinition")
  };

  pp$8.parseClassField = function(field) {
    if (checkKeyName(field, "constructor")) {
      this.raise(field.key.start, "Classes can't have a field named 'constructor'");
    } else if (field.static && checkKeyName(field, "prototype")) {
      this.raise(field.key.start, "Classes can't have a static field named 'prototype'");
    }

    if (this.eat(types$1.eq)) {
      // To raise SyntaxError if 'arguments' exists in the initializer.
      this.enterScope(SCOPE_CLASS_FIELD_INIT | SCOPE_SUPER);
      field.value = this.parseMaybeAssign();
      this.exitScope();
    } else {
      field.value = null;
    }
    this.semicolon();

    return this.finishNode(field, "PropertyDefinition")
  };

  pp$8.parseClassStaticBlock = function(node) {
    node.body = [];

    var oldLabels = this.labels;
    this.labels = [];
    this.enterScope(SCOPE_CLASS_STATIC_BLOCK | SCOPE_SUPER);
    while (this.type !== types$1.braceR) {
      var stmt = this.parseStatement(null);
      node.body.push(stmt);
    }
    this.next();
    this.exitScope();
    this.labels = oldLabels;

    return this.finishNode(node, "StaticBlock")
  };

  pp$8.parseClassId = function(node, isStatement) {
    if (this.type === types$1.name) {
      node.id = this.parseIdent();
      if (isStatement)
        { this.checkLValSimple(node.id, BIND_LEXICAL, false); }
    } else {
      if (isStatement === true)
        { this.unexpected(); }
      node.id = null;
    }
  };

  pp$8.parseClassSuper = function(node) {
    node.superClass = this.eat(types$1._extends) ? this.parseExprSubscripts(null, false) : null;
  };

  pp$8.enterClassBody = function() {
    var element = {declared: Object.create(null), used: []};
    this.privateNameStack.push(element);
    return element.declared
  };

  pp$8.exitClassBody = function() {
    var ref = this.privateNameStack.pop();
    var declared = ref.declared;
    var used = ref.used;
    if (!this.options.checkPrivateFields) { return }
    var len = this.privateNameStack.length;
    var parent = len === 0 ? null : this.privateNameStack[len - 1];
    for (var i = 0; i < used.length; ++i) {
      var id = used[i];
      if (!hasOwn(declared, id.name)) {
        if (parent) {
          parent.used.push(id);
        } else {
          this.raiseRecoverable(id.start, ("Private field '#" + (id.name) + "' must be declared in an enclosing class"));
        }
      }
    }
  };

  function isPrivateNameConflicted(privateNameMap, element) {
    var name = element.key.name;
    var curr = privateNameMap[name];

    var next = "true";
    if (element.type === "MethodDefinition" && (element.kind === "get" || element.kind === "set")) {
      next = (element.static ? "s" : "i") + element.kind;
    }

    // `class { get #a(){}; static set #a(_){} }` is also conflict.
    if (
      curr === "iget" && next === "iset" ||
      curr === "iset" && next === "iget" ||
      curr === "sget" && next === "sset" ||
      curr === "sset" && next === "sget"
    ) {
      privateNameMap[name] = "true";
      return false
    } else if (!curr) {
      privateNameMap[name] = next;
      return false
    } else {
      return true
    }
  }

  function checkKeyName(node, name) {
    var computed = node.computed;
    var key = node.key;
    return !computed && (
      key.type === "Identifier" && key.name === name ||
      key.type === "Literal" && key.value === name
    )
  }

  // Parses module export declaration.

  pp$8.parseExportAllDeclaration = function(node, exports$1) {
    if (this.options.ecmaVersion >= 11) {
      if (this.eatContextual("as")) {
        node.exported = this.parseModuleExportName();
        this.checkExport(exports$1, node.exported, this.lastTokStart);
      } else {
        node.exported = null;
      }
    }
    this.expectContextual("from");
    if (this.type !== types$1.string) { this.unexpected(); }
    node.source = this.parseExprAtom();
    if (this.options.ecmaVersion >= 16)
      { node.attributes = this.parseWithClause(); }
    this.semicolon();
    return this.finishNode(node, "ExportAllDeclaration")
  };

  pp$8.parseExport = function(node, exports$1) {
    this.next();
    // export * from '...'
    if (this.eat(types$1.star)) {
      return this.parseExportAllDeclaration(node, exports$1)
    }
    if (this.eat(types$1._default)) { // export default ...
      this.checkExport(exports$1, "default", this.lastTokStart);
      node.declaration = this.parseExportDefaultDeclaration();
      return this.finishNode(node, "ExportDefaultDeclaration")
    }
    // export var|const|let|function|class ...
    if (this.shouldParseExportStatement()) {
      node.declaration = this.parseExportDeclaration(node);
      if (node.declaration.type === "VariableDeclaration")
        { this.checkVariableExport(exports$1, node.declaration.declarations); }
      else
        { this.checkExport(exports$1, node.declaration.id, node.declaration.id.start); }
      node.specifiers = [];
      node.source = null;
      if (this.options.ecmaVersion >= 16)
        { node.attributes = []; }
    } else { // export { x, y as z } [from '...']
      node.declaration = null;
      node.specifiers = this.parseExportSpecifiers(exports$1);
      if (this.eatContextual("from")) {
        if (this.type !== types$1.string) { this.unexpected(); }
        node.source = this.parseExprAtom();
        if (this.options.ecmaVersion >= 16)
          { node.attributes = this.parseWithClause(); }
      } else {
        for (var i = 0, list = node.specifiers; i < list.length; i += 1) {
          // check for keywords used as local names
          var spec = list[i];

          this.checkUnreserved(spec.local);
          // check if export is defined
          this.checkLocalExport(spec.local);

          if (spec.local.type === "Literal") {
            this.raise(spec.local.start, "A string literal cannot be used as an exported binding without `from`.");
          }
        }

        node.source = null;
        if (this.options.ecmaVersion >= 16)
          { node.attributes = []; }
      }
      this.semicolon();
    }
    return this.finishNode(node, "ExportNamedDeclaration")
  };

  pp$8.parseExportDeclaration = function(node) {
    return this.parseStatement(null)
  };

  pp$8.parseExportDefaultDeclaration = function() {
    var isAsync;
    if (this.type === types$1._function || (isAsync = this.isAsyncFunction())) {
      var fNode = this.startNode();
      this.next();
      if (isAsync) { this.next(); }
      return this.parseFunction(fNode, FUNC_STATEMENT | FUNC_NULLABLE_ID, false, isAsync)
    } else if (this.type === types$1._class) {
      var cNode = this.startNode();
      return this.parseClass(cNode, "nullableID")
    } else {
      var declaration = this.parseMaybeAssign();
      this.semicolon();
      return declaration
    }
  };

  pp$8.checkExport = function(exports$1, name, pos) {
    if (!exports$1) { return }
    if (typeof name !== "string")
      { name = name.type === "Identifier" ? name.name : name.value; }
    if (hasOwn(exports$1, name))
      { this.raiseRecoverable(pos, "Duplicate export '" + name + "'"); }
    exports$1[name] = true;
  };

  pp$8.checkPatternExport = function(exports$1, pat) {
    var type = pat.type;
    if (type === "Identifier")
      { this.checkExport(exports$1, pat, pat.start); }
    else if (type === "ObjectPattern")
      { for (var i = 0, list = pat.properties; i < list.length; i += 1)
        {
          var prop = list[i];

          this.checkPatternExport(exports$1, prop);
        } }
    else if (type === "ArrayPattern")
      { for (var i$1 = 0, list$1 = pat.elements; i$1 < list$1.length; i$1 += 1) {
        var elt = list$1[i$1];

          if (elt) { this.checkPatternExport(exports$1, elt); }
      } }
    else if (type === "Property")
      { this.checkPatternExport(exports$1, pat.value); }
    else if (type === "AssignmentPattern")
      { this.checkPatternExport(exports$1, pat.left); }
    else if (type === "RestElement")
      { this.checkPatternExport(exports$1, pat.argument); }
  };

  pp$8.checkVariableExport = function(exports$1, decls) {
    if (!exports$1) { return }
    for (var i = 0, list = decls; i < list.length; i += 1)
      {
      var decl = list[i];

      this.checkPatternExport(exports$1, decl.id);
    }
  };

  pp$8.shouldParseExportStatement = function() {
    return this.type.keyword === "var" ||
      this.type.keyword === "const" ||
      this.type.keyword === "class" ||
      this.type.keyword === "function" ||
      this.isLet() ||
      this.isAsyncFunction()
  };

  // Parses a comma-separated list of module exports.

  pp$8.parseExportSpecifier = function(exports$1) {
    var node = this.startNode();
    node.local = this.parseModuleExportName();

    node.exported = this.eatContextual("as") ? this.parseModuleExportName() : node.local;
    this.checkExport(
      exports$1,
      node.exported,
      node.exported.start
    );

    return this.finishNode(node, "ExportSpecifier")
  };

  pp$8.parseExportSpecifiers = function(exports$1) {
    var nodes = [], first = true;
    // export { x, y as z } [from '...']
    this.expect(types$1.braceL);
    while (!this.eat(types$1.braceR)) {
      if (!first) {
        this.expect(types$1.comma);
        if (this.afterTrailingComma(types$1.braceR)) { break }
      } else { first = false; }

      nodes.push(this.parseExportSpecifier(exports$1));
    }
    return nodes
  };

  // Parses import declaration.

  pp$8.parseImport = function(node) {
    this.next();

    // import '...'
    if (this.type === types$1.string) {
      node.specifiers = empty$1;
      node.source = this.parseExprAtom();
    } else {
      node.specifiers = this.parseImportSpecifiers();
      this.expectContextual("from");
      node.source = this.type === types$1.string ? this.parseExprAtom() : this.unexpected();
    }
    if (this.options.ecmaVersion >= 16)
      { node.attributes = this.parseWithClause(); }
    this.semicolon();
    return this.finishNode(node, "ImportDeclaration")
  };

  // Parses a comma-separated list of module imports.

  pp$8.parseImportSpecifier = function() {
    var node = this.startNode();
    node.imported = this.parseModuleExportName();

    if (this.eatContextual("as")) {
      node.local = this.parseIdent();
    } else {
      this.checkUnreserved(node.imported);
      node.local = node.imported;
    }
    this.checkLValSimple(node.local, BIND_LEXICAL);

    return this.finishNode(node, "ImportSpecifier")
  };

  pp$8.parseImportDefaultSpecifier = function() {
    // import defaultObj, { x, y as z } from '...'
    var node = this.startNode();
    node.local = this.parseIdent();
    this.checkLValSimple(node.local, BIND_LEXICAL);
    return this.finishNode(node, "ImportDefaultSpecifier")
  };

  pp$8.parseImportNamespaceSpecifier = function() {
    var node = this.startNode();
    this.next();
    this.expectContextual("as");
    node.local = this.parseIdent();
    this.checkLValSimple(node.local, BIND_LEXICAL);
    return this.finishNode(node, "ImportNamespaceSpecifier")
  };

  pp$8.parseImportSpecifiers = function() {
    var nodes = [], first = true;
    if (this.type === types$1.name) {
      nodes.push(this.parseImportDefaultSpecifier());
      if (!this.eat(types$1.comma)) { return nodes }
    }
    if (this.type === types$1.star) {
      nodes.push(this.parseImportNamespaceSpecifier());
      return nodes
    }
    this.expect(types$1.braceL);
    while (!this.eat(types$1.braceR)) {
      if (!first) {
        this.expect(types$1.comma);
        if (this.afterTrailingComma(types$1.braceR)) { break }
      } else { first = false; }

      nodes.push(this.parseImportSpecifier());
    }
    return nodes
  };

  pp$8.parseWithClause = function() {
    var nodes = [];
    if (!this.eat(types$1._with)) {
      return nodes
    }
    this.expect(types$1.braceL);
    var attributeKeys = {};
    var first = true;
    while (!this.eat(types$1.braceR)) {
      if (!first) {
        this.expect(types$1.comma);
        if (this.afterTrailingComma(types$1.braceR)) { break }
      } else { first = false; }

      var attr = this.parseImportAttribute();
      var keyName = attr.key.type === "Identifier" ? attr.key.name : attr.key.value;
      if (hasOwn(attributeKeys, keyName))
        { this.raiseRecoverable(attr.key.start, "Duplicate attribute key '" + keyName + "'"); }
      attributeKeys[keyName] = true;
      nodes.push(attr);
    }
    return nodes
  };

  pp$8.parseImportAttribute = function() {
    var node = this.startNode();
    node.key = this.type === types$1.string ? this.parseExprAtom() : this.parseIdent(this.options.allowReserved !== "never");
    this.expect(types$1.colon);
    if (this.type !== types$1.string) {
      this.unexpected();
    }
    node.value = this.parseExprAtom();
    return this.finishNode(node, "ImportAttribute")
  };

  pp$8.parseModuleExportName = function() {
    if (this.options.ecmaVersion >= 13 && this.type === types$1.string) {
      var stringLiteral = this.parseLiteral(this.value);
      if (loneSurrogate.test(stringLiteral.value)) {
        this.raise(stringLiteral.start, "An export name cannot include a lone surrogate.");
      }
      return stringLiteral
    }
    return this.parseIdent(true)
  };

  // Set `ExpressionStatement#directive` property for directive prologues.
  pp$8.adaptDirectivePrologue = function(statements) {
    for (var i = 0; i < statements.length && this.isDirectiveCandidate(statements[i]); ++i) {
      statements[i].directive = statements[i].expression.raw.slice(1, -1);
    }
  };
  pp$8.isDirectiveCandidate = function(statement) {
    return (
      this.options.ecmaVersion >= 5 &&
      statement.type === "ExpressionStatement" &&
      statement.expression.type === "Literal" &&
      typeof statement.expression.value === "string" &&
      // Reject parenthesized strings.
      (this.input[statement.start] === "\"" || this.input[statement.start] === "'")
    )
  };

  var pp$7 = Parser.prototype;

  // Convert existing expression atom to assignable pattern
  // if possible.

  pp$7.toAssignable = function(node, isBinding, refDestructuringErrors) {
    if (this.options.ecmaVersion >= 6 && node) {
      switch (node.type) {
      case "Identifier":
        if (this.inAsync && node.name === "await")
          { this.raise(node.start, "Cannot use 'await' as identifier inside an async function"); }
        break

      case "ObjectPattern":
      case "ArrayPattern":
      case "AssignmentPattern":
      case "RestElement":
        break

      case "ObjectExpression":
        node.type = "ObjectPattern";
        if (refDestructuringErrors) { this.checkPatternErrors(refDestructuringErrors, true); }
        for (var i = 0, list = node.properties; i < list.length; i += 1) {
          var prop = list[i];

        this.toAssignable(prop, isBinding);
          // Early error:
          //   AssignmentRestProperty[Yield, Await] :
          //     `...` DestructuringAssignmentTarget[Yield, Await]
          //
          //   It is a Syntax Error if |DestructuringAssignmentTarget| is an |ArrayLiteral| or an |ObjectLiteral|.
          if (
            prop.type === "RestElement" &&
            (prop.argument.type === "ArrayPattern" || prop.argument.type === "ObjectPattern")
          ) {
            this.raise(prop.argument.start, "Unexpected token");
          }
        }
        break

      case "Property":
        // AssignmentProperty has type === "Property"
        if (node.kind !== "init") { this.raise(node.key.start, "Object pattern can't contain getter or setter"); }
        this.toAssignable(node.value, isBinding);
        break

      case "ArrayExpression":
        node.type = "ArrayPattern";
        if (refDestructuringErrors) { this.checkPatternErrors(refDestructuringErrors, true); }
        this.toAssignableList(node.elements, isBinding);
        break

      case "SpreadElement":
        node.type = "RestElement";
        this.toAssignable(node.argument, isBinding);
        if (node.argument.type === "AssignmentPattern")
          { this.raise(node.argument.start, "Rest elements cannot have a default value"); }
        break

      case "AssignmentExpression":
        if (node.operator !== "=") { this.raise(node.left.end, "Only '=' operator can be used for specifying default value."); }
        node.type = "AssignmentPattern";
        delete node.operator;
        this.toAssignable(node.left, isBinding);
        break

      case "ParenthesizedExpression":
        this.toAssignable(node.expression, isBinding, refDestructuringErrors);
        break

      case "ChainExpression":
        this.raiseRecoverable(node.start, "Optional chaining cannot appear in left-hand side");
        break

      case "MemberExpression":
        if (!isBinding) { break }

      default:
        this.raise(node.start, "Assigning to rvalue");
      }
    } else if (refDestructuringErrors) { this.checkPatternErrors(refDestructuringErrors, true); }
    return node
  };

  // Convert list of expression atoms to binding list.

  pp$7.toAssignableList = function(exprList, isBinding) {
    var end = exprList.length;
    for (var i = 0; i < end; i++) {
      var elt = exprList[i];
      if (elt) { this.toAssignable(elt, isBinding); }
    }
    if (end) {
      var last = exprList[end - 1];
      if (this.options.ecmaVersion === 6 && isBinding && last && last.type === "RestElement" && last.argument.type !== "Identifier")
        { this.unexpected(last.argument.start); }
    }
    return exprList
  };

  // Parses spread element.

  pp$7.parseSpread = function(refDestructuringErrors) {
    var node = this.startNode();
    this.next();
    node.argument = this.parseMaybeAssign(false, refDestructuringErrors);
    return this.finishNode(node, "SpreadElement")
  };

  pp$7.parseRestBinding = function() {
    var node = this.startNode();
    this.next();

    // RestElement inside of a function parameter must be an identifier
    if (this.options.ecmaVersion === 6 && this.type !== types$1.name)
      { this.unexpected(); }

    node.argument = this.parseBindingAtom();

    return this.finishNode(node, "RestElement")
  };

  // Parses lvalue (assignable) atom.

  pp$7.parseBindingAtom = function() {
    if (this.options.ecmaVersion >= 6) {
      switch (this.type) {
      case types$1.bracketL:
        var node = this.startNode();
        this.next();
        node.elements = this.parseBindingList(types$1.bracketR, true, true);
        return this.finishNode(node, "ArrayPattern")

      case types$1.braceL:
        return this.parseObj(true)
      }
    }
    return this.parseIdent()
  };

  pp$7.parseBindingList = function(close, allowEmpty, allowTrailingComma, allowModifiers) {
    var elts = [], first = true;
    while (!this.eat(close)) {
      if (first) { first = false; }
      else { this.expect(types$1.comma); }
      if (allowEmpty && this.type === types$1.comma) {
        elts.push(null);
      } else if (allowTrailingComma && this.afterTrailingComma(close)) {
        break
      } else if (this.type === types$1.ellipsis) {
        var rest = this.parseRestBinding();
        this.parseBindingListItem(rest);
        elts.push(rest);
        if (this.type === types$1.comma) { this.raiseRecoverable(this.start, "Comma is not permitted after the rest element"); }
        this.expect(close);
        break
      } else {
        elts.push(this.parseAssignableListItem(allowModifiers));
      }
    }
    return elts
  };

  pp$7.parseAssignableListItem = function(allowModifiers) {
    var elem = this.parseMaybeDefault(this.start, this.startLoc);
    this.parseBindingListItem(elem);
    return elem
  };

  pp$7.parseBindingListItem = function(param) {
    return param
  };

  // Parses assignment pattern around given atom if possible.

  pp$7.parseMaybeDefault = function(startPos, startLoc, left) {
    left = left || this.parseBindingAtom();
    if (this.options.ecmaVersion < 6 || !this.eat(types$1.eq)) { return left }
    var node = this.startNodeAt(startPos, startLoc);
    node.left = left;
    node.right = this.parseMaybeAssign();
    return this.finishNode(node, "AssignmentPattern")
  };

  // The following three functions all verify that a node is an lvalue —
  // something that can be bound, or assigned to. In order to do so, they perform
  // a variety of checks:
  //
  // - Check that none of the bound/assigned-to identifiers are reserved words.
  // - Record name declarations for bindings in the appropriate scope.
  // - Check duplicate argument names, if checkClashes is set.
  //
  // If a complex binding pattern is encountered (e.g., object and array
  // destructuring), the entire pattern is recursively checked.
  //
  // There are three versions of checkLVal*() appropriate for different
  // circumstances:
  //
  // - checkLValSimple() shall be used if the syntactic construct supports
  //   nothing other than identifiers and member expressions. Parenthesized
  //   expressions are also correctly handled. This is generally appropriate for
  //   constructs for which the spec says
  //
  //   > It is a Syntax Error if AssignmentTargetType of [the production] is not
  //   > simple.
  //
  //   It is also appropriate for checking if an identifier is valid and not
  //   defined elsewhere, like import declarations or function/class identifiers.
  //
  //   Examples where this is used include:
  //     a += …;
  //     import a from '…';
  //   where a is the node to be checked.
  //
  // - checkLValPattern() shall be used if the syntactic construct supports
  //   anything checkLValSimple() supports, as well as object and array
  //   destructuring patterns. This is generally appropriate for constructs for
  //   which the spec says
  //
  //   > It is a Syntax Error if [the production] is neither an ObjectLiteral nor
  //   > an ArrayLiteral and AssignmentTargetType of [the production] is not
  //   > simple.
  //
  //   Examples where this is used include:
  //     (a = …);
  //     const a = …;
  //     try { … } catch (a) { … }
  //   where a is the node to be checked.
  //
  // - checkLValInnerPattern() shall be used if the syntactic construct supports
  //   anything checkLValPattern() supports, as well as default assignment
  //   patterns, rest elements, and other constructs that may appear within an
  //   object or array destructuring pattern.
  //
  //   As a special case, function parameters also use checkLValInnerPattern(),
  //   as they also support defaults and rest constructs.
  //
  // These functions deliberately support both assignment and binding constructs,
  // as the logic for both is exceedingly similar. If the node is the target of
  // an assignment, then bindingType should be set to BIND_NONE. Otherwise, it
  // should be set to the appropriate BIND_* constant, like BIND_VAR or
  // BIND_LEXICAL.
  //
  // If the function is called with a non-BIND_NONE bindingType, then
  // additionally a checkClashes object may be specified to allow checking for
  // duplicate argument names. checkClashes is ignored if the provided construct
  // is an assignment (i.e., bindingType is BIND_NONE).

  pp$7.checkLValSimple = function(expr, bindingType, checkClashes) {
    if ( bindingType === void 0 ) bindingType = BIND_NONE;

    var isBind = bindingType !== BIND_NONE;

    switch (expr.type) {
    case "Identifier":
      if (this.strict && this.reservedWordsStrictBind.test(expr.name))
        { this.raiseRecoverable(expr.start, (isBind ? "Binding " : "Assigning to ") + expr.name + " in strict mode"); }
      if (isBind) {
        if (bindingType === BIND_LEXICAL && expr.name === "let")
          { this.raiseRecoverable(expr.start, "let is disallowed as a lexically bound name"); }
        if (checkClashes) {
          if (hasOwn(checkClashes, expr.name))
            { this.raiseRecoverable(expr.start, "Argument name clash"); }
          checkClashes[expr.name] = true;
        }
        if (bindingType !== BIND_OUTSIDE) { this.declareName(expr.name, bindingType, expr.start); }
      }
      break

    case "ChainExpression":
      this.raiseRecoverable(expr.start, "Optional chaining cannot appear in left-hand side");
      break

    case "MemberExpression":
      if (isBind) { this.raiseRecoverable(expr.start, "Binding member expression"); }
      break

    case "ParenthesizedExpression":
      if (isBind) { this.raiseRecoverable(expr.start, "Binding parenthesized expression"); }
      return this.checkLValSimple(expr.expression, bindingType, checkClashes)

    default:
      this.raise(expr.start, (isBind ? "Binding" : "Assigning to") + " rvalue");
    }
  };

  pp$7.checkLValPattern = function(expr, bindingType, checkClashes) {
    if ( bindingType === void 0 ) bindingType = BIND_NONE;

    switch (expr.type) {
    case "ObjectPattern":
      for (var i = 0, list = expr.properties; i < list.length; i += 1) {
        var prop = list[i];

      this.checkLValInnerPattern(prop, bindingType, checkClashes);
      }
      break

    case "ArrayPattern":
      for (var i$1 = 0, list$1 = expr.elements; i$1 < list$1.length; i$1 += 1) {
        var elem = list$1[i$1];

      if (elem) { this.checkLValInnerPattern(elem, bindingType, checkClashes); }
      }
      break

    default:
      this.checkLValSimple(expr, bindingType, checkClashes);
    }
  };

  pp$7.checkLValInnerPattern = function(expr, bindingType, checkClashes) {
    if ( bindingType === void 0 ) bindingType = BIND_NONE;

    switch (expr.type) {
    case "Property":
      // AssignmentProperty has type === "Property"
      this.checkLValInnerPattern(expr.value, bindingType, checkClashes);
      break

    case "AssignmentPattern":
      this.checkLValPattern(expr.left, bindingType, checkClashes);
      break

    case "RestElement":
      this.checkLValPattern(expr.argument, bindingType, checkClashes);
      break

    default:
      this.checkLValPattern(expr, bindingType, checkClashes);
    }
  };

  // The algorithm used to determine whether a regexp can appear at a
  // given point in the program is loosely based on sweet.js' approach.
  // See https://github.com/mozilla/sweet.js/wiki/design


  var TokContext = function TokContext(token, isExpr, preserveSpace, override, generator) {
    this.token = token;
    this.isExpr = !!isExpr;
    this.preserveSpace = !!preserveSpace;
    this.override = override;
    this.generator = !!generator;
  };

  var types = {
    b_stat: new TokContext("{", false),
    b_expr: new TokContext("{", true),
    b_tmpl: new TokContext("${", false),
    p_stat: new TokContext("(", false),
    p_expr: new TokContext("(", true),
    q_tmpl: new TokContext("`", true, true, function (p) { return p.tryReadTemplateToken(); }),
    f_stat: new TokContext("function", false),
    f_expr: new TokContext("function", true),
    f_expr_gen: new TokContext("function", true, false, null, true),
    f_gen: new TokContext("function", false, false, null, true)
  };

  var pp$6 = Parser.prototype;

  pp$6.initialContext = function() {
    return [types.b_stat]
  };

  pp$6.curContext = function() {
    return this.context[this.context.length - 1]
  };

  pp$6.braceIsBlock = function(prevType) {
    var parent = this.curContext();
    if (parent === types.f_expr || parent === types.f_stat)
      { return true }
    if (prevType === types$1.colon && (parent === types.b_stat || parent === types.b_expr))
      { return !parent.isExpr }

    // The check for `tt.name && exprAllowed` detects whether we are
    // after a `yield` or `of` construct. See the `updateContext` for
    // `tt.name`.
    if (prevType === types$1._return || prevType === types$1.name && this.exprAllowed)
      { return lineBreak.test(this.input.slice(this.lastTokEnd, this.start)) }
    if (prevType === types$1._else || prevType === types$1.semi || prevType === types$1.eof || prevType === types$1.parenR || prevType === types$1.arrow)
      { return true }
    if (prevType === types$1.braceL)
      { return parent === types.b_stat }
    if (prevType === types$1._var || prevType === types$1._const || prevType === types$1.name)
      { return false }
    return !this.exprAllowed
  };

  pp$6.inGeneratorContext = function() {
    for (var i = this.context.length - 1; i >= 1; i--) {
      var context = this.context[i];
      if (context.token === "function")
        { return context.generator }
    }
    return false
  };

  pp$6.updateContext = function(prevType) {
    var update, type = this.type;
    if (type.keyword && prevType === types$1.dot)
      { this.exprAllowed = false; }
    else if (update = type.updateContext)
      { update.call(this, prevType); }
    else
      { this.exprAllowed = type.beforeExpr; }
  };

  // Used to handle edge cases when token context could not be inferred correctly during tokenization phase

  pp$6.overrideContext = function(tokenCtx) {
    if (this.curContext() !== tokenCtx) {
      this.context[this.context.length - 1] = tokenCtx;
    }
  };

  // Token-specific context update code

  types$1.parenR.updateContext = types$1.braceR.updateContext = function() {
    if (this.context.length === 1) {
      this.exprAllowed = true;
      return
    }
    var out = this.context.pop();
    if (out === types.b_stat && this.curContext().token === "function") {
      out = this.context.pop();
    }
    this.exprAllowed = !out.isExpr;
  };

  types$1.braceL.updateContext = function(prevType) {
    this.context.push(this.braceIsBlock(prevType) ? types.b_stat : types.b_expr);
    this.exprAllowed = true;
  };

  types$1.dollarBraceL.updateContext = function() {
    this.context.push(types.b_tmpl);
    this.exprAllowed = true;
  };

  types$1.parenL.updateContext = function(prevType) {
    var statementParens = prevType === types$1._if || prevType === types$1._for || prevType === types$1._with || prevType === types$1._while;
    this.context.push(statementParens ? types.p_stat : types.p_expr);
    this.exprAllowed = true;
  };

  types$1.incDec.updateContext = function() {
    // tokExprAllowed stays unchanged
  };

  types$1._function.updateContext = types$1._class.updateContext = function(prevType) {
    if (prevType.beforeExpr && prevType !== types$1._else &&
        !(prevType === types$1.semi && this.curContext() !== types.p_stat) &&
        !(prevType === types$1._return && lineBreak.test(this.input.slice(this.lastTokEnd, this.start))) &&
        !((prevType === types$1.colon || prevType === types$1.braceL) && this.curContext() === types.b_stat))
      { this.context.push(types.f_expr); }
    else
      { this.context.push(types.f_stat); }
    this.exprAllowed = false;
  };

  types$1.colon.updateContext = function() {
    if (this.curContext().token === "function") { this.context.pop(); }
    this.exprAllowed = true;
  };

  types$1.backQuote.updateContext = function() {
    if (this.curContext() === types.q_tmpl)
      { this.context.pop(); }
    else
      { this.context.push(types.q_tmpl); }
    this.exprAllowed = false;
  };

  types$1.star.updateContext = function(prevType) {
    if (prevType === types$1._function) {
      var index = this.context.length - 1;
      if (this.context[index] === types.f_expr)
        { this.context[index] = types.f_expr_gen; }
      else
        { this.context[index] = types.f_gen; }
    }
    this.exprAllowed = true;
  };

  types$1.name.updateContext = function(prevType) {
    var allowed = false;
    if (this.options.ecmaVersion >= 6 && prevType !== types$1.dot) {
      if (this.value === "of" && !this.exprAllowed ||
          this.value === "yield" && this.inGeneratorContext())
        { allowed = true; }
    }
    this.exprAllowed = allowed;
  };

  // A recursive descent parser operates by defining functions for all
  // syntactic elements, and recursively calling those, each function
  // advancing the input stream and returning an AST node. Precedence
  // of constructs (for example, the fact that `!x[1]` means `!(x[1])`
  // instead of `(!x)[1]` is handled by the fact that the parser
  // function that parses unary prefix operators is called first, and
  // in turn calls the function that parses `[]` subscripts — that
  // way, it'll receive the node for `x[1]` already parsed, and wraps
  // *that* in the unary operator node.
  //
  // Acorn uses an [operator precedence parser][opp] to handle binary
  // operator precedence, because it is much more compact than using
  // the technique outlined above, which uses different, nesting
  // functions to specify precedence, for all of the ten binary
  // precedence levels that JavaScript defines.
  //
  // [opp]: http://en.wikipedia.org/wiki/Operator-precedence_parser


  var pp$5 = Parser.prototype;

  // Check if property name clashes with already added.
  // Object/class getters and setters are not allowed to clash —
  // either with each other or with an init property — and in
  // strict mode, init properties are also not allowed to be repeated.

  pp$5.checkPropClash = function(prop, propHash, refDestructuringErrors) {
    if (this.options.ecmaVersion >= 9 && prop.type === "SpreadElement")
      { return }
    if (this.options.ecmaVersion >= 6 && (prop.computed || prop.method || prop.shorthand))
      { return }
    var key = prop.key;
    var name;
    switch (key.type) {
    case "Identifier": name = key.name; break
    case "Literal": name = String(key.value); break
    default: return
    }
    var kind = prop.kind;
    if (this.options.ecmaVersion >= 6) {
      if (name === "__proto__" && kind === "init") {
        if (propHash.proto) {
          if (refDestructuringErrors) {
            if (refDestructuringErrors.doubleProto < 0) {
              refDestructuringErrors.doubleProto = key.start;
            }
          } else {
            this.raiseRecoverable(key.start, "Redefinition of __proto__ property");
          }
        }
        propHash.proto = true;
      }
      return
    }
    name = "$" + name;
    var other = propHash[name];
    if (other) {
      var redefinition;
      if (kind === "init") {
        redefinition = this.strict && other.init || other.get || other.set;
      } else {
        redefinition = other.init || other[kind];
      }
      if (redefinition)
        { this.raiseRecoverable(key.start, "Redefinition of property"); }
    } else {
      other = propHash[name] = {
        init: false,
        get: false,
        set: false
      };
    }
    other[kind] = true;
  };

  // ### Expression parsing

  // These nest, from the most general expression type at the top to
  // 'atomic', nondivisible expression types at the bottom. Most of
  // the functions will simply let the function(s) below them parse,
  // and, *if* the syntactic construct they handle is present, wrap
  // the AST node that the inner parser gave them in another node.

  // Parse a full expression. The optional arguments are used to
  // forbid the `in` operator (in for loops initalization expressions)
  // and provide reference for storing '=' operator inside shorthand
  // property assignment in contexts where both object expression
  // and object pattern might appear (so it's possible to raise
  // delayed syntax error at correct position).

  pp$5.parseExpression = function(forInit, refDestructuringErrors) {
    var startPos = this.start, startLoc = this.startLoc;
    var expr = this.parseMaybeAssign(forInit, refDestructuringErrors);
    if (this.type === types$1.comma) {
      var node = this.startNodeAt(startPos, startLoc);
      node.expressions = [expr];
      while (this.eat(types$1.comma)) { node.expressions.push(this.parseMaybeAssign(forInit, refDestructuringErrors)); }
      return this.finishNode(node, "SequenceExpression")
    }
    return expr
  };

  // Parse an assignment expression. This includes applications of
  // operators like `+=`.

  pp$5.parseMaybeAssign = function(forInit, refDestructuringErrors, afterLeftParse) {
    if (this.isContextual("yield")) {
      if (this.inGenerator) { return this.parseYield(forInit) }
      // The tokenizer will assume an expression is allowed after
      // `yield`, but this isn't that kind of yield
      else { this.exprAllowed = false; }
    }

    var ownDestructuringErrors = false, oldParenAssign = -1, oldTrailingComma = -1, oldDoubleProto = -1;
    if (refDestructuringErrors) {
      oldParenAssign = refDestructuringErrors.parenthesizedAssign;
      oldTrailingComma = refDestructuringErrors.trailingComma;
      oldDoubleProto = refDestructuringErrors.doubleProto;
      refDestructuringErrors.parenthesizedAssign = refDestructuringErrors.trailingComma = -1;
    } else {
      refDestructuringErrors = new DestructuringErrors;
      ownDestructuringErrors = true;
    }

    var startPos = this.start, startLoc = this.startLoc;
    if (this.type === types$1.parenL || this.type === types$1.name) {
      this.potentialArrowAt = this.start;
      this.potentialArrowInForAwait = forInit === "await";
    }
    var left = this.parseMaybeConditional(forInit, refDestructuringErrors);
    if (afterLeftParse) { left = afterLeftParse.call(this, left, startPos, startLoc); }
    if (this.type.isAssign) {
      var node = this.startNodeAt(startPos, startLoc);
      node.operator = this.value;
      if (this.type === types$1.eq)
        { left = this.toAssignable(left, false, refDestructuringErrors); }
      if (!ownDestructuringErrors) {
        refDestructuringErrors.parenthesizedAssign = refDestructuringErrors.trailingComma = refDestructuringErrors.doubleProto = -1;
      }
      if (refDestructuringErrors.shorthandAssign >= left.start)
        { refDestructuringErrors.shorthandAssign = -1; } // reset because shorthand default was used correctly
      if (this.type === types$1.eq)
        { this.checkLValPattern(left); }
      else
        { this.checkLValSimple(left); }
      node.left = left;
      this.next();
      node.right = this.parseMaybeAssign(forInit);
      if (oldDoubleProto > -1) { refDestructuringErrors.doubleProto = oldDoubleProto; }
      return this.finishNode(node, "AssignmentExpression")
    } else {
      if (ownDestructuringErrors) { this.checkExpressionErrors(refDestructuringErrors, true); }
    }
    if (oldParenAssign > -1) { refDestructuringErrors.parenthesizedAssign = oldParenAssign; }
    if (oldTrailingComma > -1) { refDestructuringErrors.trailingComma = oldTrailingComma; }
    return left
  };

  // Parse a ternary conditional (`?:`) operator.

  pp$5.parseMaybeConditional = function(forInit, refDestructuringErrors) {
    var startPos = this.start, startLoc = this.startLoc;
    var expr = this.parseExprOps(forInit, refDestructuringErrors);
    if (this.checkExpressionErrors(refDestructuringErrors)) { return expr }
    if (this.eat(types$1.question)) {
      var node = this.startNodeAt(startPos, startLoc);
      node.test = expr;
      node.consequent = this.parseMaybeAssign();
      this.expect(types$1.colon);
      node.alternate = this.parseMaybeAssign(forInit);
      return this.finishNode(node, "ConditionalExpression")
    }
    return expr
  };

  // Start the precedence parser.

  pp$5.parseExprOps = function(forInit, refDestructuringErrors) {
    var startPos = this.start, startLoc = this.startLoc;
    var expr = this.parseMaybeUnary(refDestructuringErrors, false, false, forInit);
    if (this.checkExpressionErrors(refDestructuringErrors)) { return expr }
    return expr.start === startPos && expr.type === "ArrowFunctionExpression" ? expr : this.parseExprOp(expr, startPos, startLoc, -1, forInit)
  };

  // Parse binary operators with the operator precedence parsing
  // algorithm. `left` is the left-hand side of the operator.
  // `minPrec` provides context that allows the function to stop and
  // defer further parser to one of its callers when it encounters an
  // operator that has a lower precedence than the set it is parsing.

  pp$5.parseExprOp = function(left, leftStartPos, leftStartLoc, minPrec, forInit) {
    var prec = this.type.binop;
    if (prec != null && (!forInit || this.type !== types$1._in)) {
      if (prec > minPrec) {
        var logical = this.type === types$1.logicalOR || this.type === types$1.logicalAND;
        var coalesce = this.type === types$1.coalesce;
        if (coalesce) {
          // Handle the precedence of `tt.coalesce` as equal to the range of logical expressions.
          // In other words, `node.right` shouldn't contain logical expressions in order to check the mixed error.
          prec = types$1.logicalAND.binop;
        }
        var op = this.value;
        this.next();
        var startPos = this.start, startLoc = this.startLoc;
        var right = this.parseExprOp(this.parseMaybeUnary(null, false, false, forInit), startPos, startLoc, prec, forInit);
        var node = this.buildBinary(leftStartPos, leftStartLoc, left, right, op, logical || coalesce);
        if ((logical && this.type === types$1.coalesce) || (coalesce && (this.type === types$1.logicalOR || this.type === types$1.logicalAND))) {
          this.raiseRecoverable(this.start, "Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses");
        }
        return this.parseExprOp(node, leftStartPos, leftStartLoc, minPrec, forInit)
      }
    }
    return left
  };

  pp$5.buildBinary = function(startPos, startLoc, left, right, op, logical) {
    if (right.type === "PrivateIdentifier") { this.raise(right.start, "Private identifier can only be left side of binary expression"); }
    var node = this.startNodeAt(startPos, startLoc);
    node.left = left;
    node.operator = op;
    node.right = right;
    return this.finishNode(node, logical ? "LogicalExpression" : "BinaryExpression")
  };

  // Parse unary operators, both prefix and postfix.

  pp$5.parseMaybeUnary = function(refDestructuringErrors, sawUnary, incDec, forInit) {
    var startPos = this.start, startLoc = this.startLoc, expr;
    if (this.isContextual("await") && this.canAwait) {
      expr = this.parseAwait(forInit);
      sawUnary = true;
    } else if (this.type.prefix) {
      var node = this.startNode(), update = this.type === types$1.incDec;
      node.operator = this.value;
      node.prefix = true;
      this.next();
      node.argument = this.parseMaybeUnary(null, true, update, forInit);
      this.checkExpressionErrors(refDestructuringErrors, true);
      if (update) { this.checkLValSimple(node.argument); }
      else if (this.strict && node.operator === "delete" && isLocalVariableAccess(node.argument))
        { this.raiseRecoverable(node.start, "Deleting local variable in strict mode"); }
      else if (node.operator === "delete" && isPrivateFieldAccess(node.argument))
        { this.raiseRecoverable(node.start, "Private fields can not be deleted"); }
      else { sawUnary = true; }
      expr = this.finishNode(node, update ? "UpdateExpression" : "UnaryExpression");
    } else if (!sawUnary && this.type === types$1.privateId) {
      if ((forInit || this.privateNameStack.length === 0) && this.options.checkPrivateFields) { this.unexpected(); }
      expr = this.parsePrivateIdent();
      // only could be private fields in 'in', such as #x in obj
      if (this.type !== types$1._in) { this.unexpected(); }
    } else {
      expr = this.parseExprSubscripts(refDestructuringErrors, forInit);
      if (this.checkExpressionErrors(refDestructuringErrors)) { return expr }
      while (this.type.postfix && !this.canInsertSemicolon()) {
        var node$1 = this.startNodeAt(startPos, startLoc);
        node$1.operator = this.value;
        node$1.prefix = false;
        node$1.argument = expr;
        this.checkLValSimple(expr);
        this.next();
        expr = this.finishNode(node$1, "UpdateExpression");
      }
    }

    if (!incDec && this.eat(types$1.starstar)) {
      if (sawUnary)
        { this.unexpected(this.lastTokStart); }
      else
        { return this.buildBinary(startPos, startLoc, expr, this.parseMaybeUnary(null, false, false, forInit), "**", false) }
    } else {
      return expr
    }
  };

  function isLocalVariableAccess(node) {
    return (
      node.type === "Identifier" ||
      node.type === "ParenthesizedExpression" && isLocalVariableAccess(node.expression)
    )
  }

  function isPrivateFieldAccess(node) {
    return (
      node.type === "MemberExpression" && node.property.type === "PrivateIdentifier" ||
      node.type === "ChainExpression" && isPrivateFieldAccess(node.expression) ||
      node.type === "ParenthesizedExpression" && isPrivateFieldAccess(node.expression)
    )
  }

  // Parse call, dot, and `[]`-subscript expressions.

  pp$5.parseExprSubscripts = function(refDestructuringErrors, forInit) {
    var startPos = this.start, startLoc = this.startLoc;
    var expr = this.parseExprAtom(refDestructuringErrors, forInit);
    if (expr.type === "ArrowFunctionExpression" && this.input.slice(this.lastTokStart, this.lastTokEnd) !== ")")
      { return expr }
    var result = this.parseSubscripts(expr, startPos, startLoc, false, forInit);
    if (refDestructuringErrors && result.type === "MemberExpression") {
      if (refDestructuringErrors.parenthesizedAssign >= result.start) { refDestructuringErrors.parenthesizedAssign = -1; }
      if (refDestructuringErrors.parenthesizedBind >= result.start) { refDestructuringErrors.parenthesizedBind = -1; }
      if (refDestructuringErrors.trailingComma >= result.start) { refDestructuringErrors.trailingComma = -1; }
    }
    return result
  };

  pp$5.parseSubscripts = function(base, startPos, startLoc, noCalls, forInit) {
    var maybeAsyncArrow = this.options.ecmaVersion >= 8 && base.type === "Identifier" && base.name === "async" &&
        this.lastTokEnd === base.end && !this.canInsertSemicolon() && base.end - base.start === 5 &&
        this.potentialArrowAt === base.start;
    var optionalChained = false;

    while (true) {
      var element = this.parseSubscript(base, startPos, startLoc, noCalls, maybeAsyncArrow, optionalChained, forInit);

      if (element.optional) { optionalChained = true; }
      if (element === base || element.type === "ArrowFunctionExpression") {
        if (optionalChained) {
          var chainNode = this.startNodeAt(startPos, startLoc);
          chainNode.expression = element;
          element = this.finishNode(chainNode, "ChainExpression");
        }
        return element
      }

      base = element;
    }
  };

  pp$5.shouldParseAsyncArrow = function() {
    return !this.canInsertSemicolon() && this.eat(types$1.arrow)
  };

  pp$5.parseSubscriptAsyncArrow = function(startPos, startLoc, exprList, forInit) {
    return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), exprList, true, forInit)
  };

  pp$5.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow, optionalChained, forInit) {
    var optionalSupported = this.options.ecmaVersion >= 11;
    var optional = optionalSupported && this.eat(types$1.questionDot);
    if (noCalls && optional) { this.raise(this.lastTokStart, "Optional chaining cannot appear in the callee of new expressions"); }

    var computed = this.eat(types$1.bracketL);
    if (computed || (optional && this.type !== types$1.parenL && this.type !== types$1.backQuote) || this.eat(types$1.dot)) {
      var node = this.startNodeAt(startPos, startLoc);
      node.object = base;
      if (computed) {
        node.property = this.parseExpression();
        this.expect(types$1.bracketR);
      } else if (this.type === types$1.privateId && base.type !== "Super") {
        node.property = this.parsePrivateIdent();
      } else {
        node.property = this.parseIdent(this.options.allowReserved !== "never");
      }
      node.computed = !!computed;
      if (optionalSupported) {
        node.optional = optional;
      }
      base = this.finishNode(node, "MemberExpression");
    } else if (!noCalls && this.eat(types$1.parenL)) {
      var refDestructuringErrors = new DestructuringErrors, oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldAwaitIdentPos = this.awaitIdentPos;
      this.yieldPos = 0;
      this.awaitPos = 0;
      this.awaitIdentPos = 0;
      var exprList = this.parseExprList(types$1.parenR, this.options.ecmaVersion >= 8, false, refDestructuringErrors);
      if (maybeAsyncArrow && !optional && this.shouldParseAsyncArrow()) {
        this.checkPatternErrors(refDestructuringErrors, false);
        this.checkYieldAwaitInDefaultParams();
        if (this.awaitIdentPos > 0)
          { this.raise(this.awaitIdentPos, "Cannot use 'await' as identifier inside an async function"); }
        this.yieldPos = oldYieldPos;
        this.awaitPos = oldAwaitPos;
        this.awaitIdentPos = oldAwaitIdentPos;
        return this.parseSubscriptAsyncArrow(startPos, startLoc, exprList, forInit)
      }
      this.checkExpressionErrors(refDestructuringErrors, true);
      this.yieldPos = oldYieldPos || this.yieldPos;
      this.awaitPos = oldAwaitPos || this.awaitPos;
      this.awaitIdentPos = oldAwaitIdentPos || this.awaitIdentPos;
      var node$1 = this.startNodeAt(startPos, startLoc);
      node$1.callee = base;
      node$1.arguments = exprList;
      if (optionalSupported) {
        node$1.optional = optional;
      }
      base = this.finishNode(node$1, "CallExpression");
    } else if (this.type === types$1.backQuote) {
      if (optional || optionalChained) {
        this.raise(this.start, "Optional chaining cannot appear in the tag of tagged template expressions");
      }
      var node$2 = this.startNodeAt(startPos, startLoc);
      node$2.tag = base;
      node$2.quasi = this.parseTemplate({isTagged: true});
      base = this.finishNode(node$2, "TaggedTemplateExpression");
    }
    return base
  };

  // Parse an atomic expression — either a single token that is an
  // expression, an expression started by a keyword like `function` or
  // `new`, or an expression wrapped in punctuation like `()`, `[]`,
  // or `{}`.

  pp$5.parseExprAtom = function(refDestructuringErrors, forInit, forNew) {
    // If a division operator appears in an expression position, the
    // tokenizer got confused, and we force it to read a regexp instead.
    if (this.type === types$1.slash) { this.readRegexp(); }

    var node, canBeArrow = this.potentialArrowAt === this.start;
    switch (this.type) {
    case types$1._super:
      if (!this.allowSuper)
        { this.raise(this.start, "'super' keyword outside a method"); }
      node = this.startNode();
      this.next();
      if (this.type === types$1.parenL && !this.allowDirectSuper)
        { this.raise(node.start, "super() call outside constructor of a subclass"); }
      // The `super` keyword can appear at below:
      // SuperProperty:
      //     super [ Expression ]
      //     super . IdentifierName
      // SuperCall:
      //     super ( Arguments )
      if (this.type !== types$1.dot && this.type !== types$1.bracketL && this.type !== types$1.parenL)
        { this.unexpected(); }
      return this.finishNode(node, "Super")

    case types$1._this:
      node = this.startNode();
      this.next();
      return this.finishNode(node, "ThisExpression")

    case types$1.name:
      var startPos = this.start, startLoc = this.startLoc, containsEsc = this.containsEsc;
      var id = this.parseIdent(false);
      if (this.options.ecmaVersion >= 8 && !containsEsc && id.name === "async" && !this.canInsertSemicolon() && this.eat(types$1._function)) {
        this.overrideContext(types.f_expr);
        return this.parseFunction(this.startNodeAt(startPos, startLoc), 0, false, true, forInit)
      }
      if (canBeArrow && !this.canInsertSemicolon()) {
        if (this.eat(types$1.arrow))
          { return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), [id], false, forInit) }
        if (this.options.ecmaVersion >= 8 && id.name === "async" && this.type === types$1.name && !containsEsc &&
            (!this.potentialArrowInForAwait || this.value !== "of" || this.containsEsc)) {
          id = this.parseIdent(false);
          if (this.canInsertSemicolon() || !this.eat(types$1.arrow))
            { this.unexpected(); }
          return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), [id], true, forInit)
        }
      }
      return id

    case types$1.regexp:
      var value = this.value;
      node = this.parseLiteral(value.value);
      node.regex = {pattern: value.pattern, flags: value.flags};
      return node

    case types$1.num: case types$1.string:
      return this.parseLiteral(this.value)

    case types$1._null: case types$1._true: case types$1._false:
      node = this.startNode();
      node.value = this.type === types$1._null ? null : this.type === types$1._true;
      node.raw = this.type.keyword;
      this.next();
      return this.finishNode(node, "Literal")

    case types$1.parenL:
      var start = this.start, expr = this.parseParenAndDistinguishExpression(canBeArrow, forInit);
      if (refDestructuringErrors) {
        if (refDestructuringErrors.parenthesizedAssign < 0 && !this.isSimpleAssignTarget(expr))
          { refDestructuringErrors.parenthesizedAssign = start; }
        if (refDestructuringErrors.parenthesizedBind < 0)
          { refDestructuringErrors.parenthesizedBind = start; }
      }
      return expr

    case types$1.bracketL:
      node = this.startNode();
      this.next();
      node.elements = this.parseExprList(types$1.bracketR, true, true, refDestructuringErrors);
      return this.finishNode(node, "ArrayExpression")

    case types$1.braceL:
      this.overrideContext(types.b_expr);
      return this.parseObj(false, refDestructuringErrors)

    case types$1._function:
      node = this.startNode();
      this.next();
      return this.parseFunction(node, 0)

    case types$1._class:
      return this.parseClass(this.startNode(), false)

    case types$1._new:
      return this.parseNew()

    case types$1.backQuote:
      return this.parseTemplate()

    case types$1._import:
      if (this.options.ecmaVersion >= 11) {
        return this.parseExprImport(forNew)
      } else {
        return this.unexpected()
      }

    default:
      return this.parseExprAtomDefault()
    }
  };

  pp$5.parseExprAtomDefault = function() {
    this.unexpected();
  };

  pp$5.parseExprImport = function(forNew) {
    var node = this.startNode();

    // Consume `import` as an identifier for `import.meta`.
    // Because `this.parseIdent(true)` doesn't check escape sequences, it needs the check of `this.containsEsc`.
    if (this.containsEsc) { this.raiseRecoverable(this.start, "Escape sequence in keyword import"); }
    this.next();

    if (this.type === types$1.parenL && !forNew) {
      return this.parseDynamicImport(node)
    } else if (this.type === types$1.dot) {
      var meta = this.startNodeAt(node.start, node.loc && node.loc.start);
      meta.name = "import";
      node.meta = this.finishNode(meta, "Identifier");
      return this.parseImportMeta(node)
    } else {
      this.unexpected();
    }
  };

  pp$5.parseDynamicImport = function(node) {
    this.next(); // skip `(`

    // Parse node.source.
    node.source = this.parseMaybeAssign();

    if (this.options.ecmaVersion >= 16) {
      if (!this.eat(types$1.parenR)) {
        this.expect(types$1.comma);
        if (!this.afterTrailingComma(types$1.parenR)) {
          node.options = this.parseMaybeAssign();
          if (!this.eat(types$1.parenR)) {
            this.expect(types$1.comma);
            if (!this.afterTrailingComma(types$1.parenR)) {
              this.unexpected();
            }
          }
        } else {
          node.options = null;
        }
      } else {
        node.options = null;
      }
    } else {
      // Verify ending.
      if (!this.eat(types$1.parenR)) {
        var errorPos = this.start;
        if (this.eat(types$1.comma) && this.eat(types$1.parenR)) {
          this.raiseRecoverable(errorPos, "Trailing comma is not allowed in import()");
        } else {
          this.unexpected(errorPos);
        }
      }
    }

    return this.finishNode(node, "ImportExpression")
  };

  pp$5.parseImportMeta = function(node) {
    this.next(); // skip `.`

    var containsEsc = this.containsEsc;
    node.property = this.parseIdent(true);

    if (node.property.name !== "meta")
      { this.raiseRecoverable(node.property.start, "The only valid meta property for import is 'import.meta'"); }
    if (containsEsc)
      { this.raiseRecoverable(node.start, "'import.meta' must not contain escaped characters"); }
    if (this.options.sourceType !== "module" && !this.options.allowImportExportEverywhere)
      { this.raiseRecoverable(node.start, "Cannot use 'import.meta' outside a module"); }

    return this.finishNode(node, "MetaProperty")
  };

  pp$5.parseLiteral = function(value) {
    var node = this.startNode();
    node.value = value;
    node.raw = this.input.slice(this.start, this.end);
    if (node.raw.charCodeAt(node.raw.length - 1) === 110)
      { node.bigint = node.value != null ? node.value.toString() : node.raw.slice(0, -1).replace(/_/g, ""); }
    this.next();
    return this.finishNode(node, "Literal")
  };

  pp$5.parseParenExpression = function() {
    this.expect(types$1.parenL);
    var val = this.parseExpression();
    this.expect(types$1.parenR);
    return val
  };

  pp$5.shouldParseArrow = function(exprList) {
    return !this.canInsertSemicolon()
  };

  pp$5.parseParenAndDistinguishExpression = function(canBeArrow, forInit) {
    var startPos = this.start, startLoc = this.startLoc, val, allowTrailingComma = this.options.ecmaVersion >= 8;
    if (this.options.ecmaVersion >= 6) {
      this.next();

      var innerStartPos = this.start, innerStartLoc = this.startLoc;
      var exprList = [], first = true, lastIsComma = false;
      var refDestructuringErrors = new DestructuringErrors, oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, spreadStart;
      this.yieldPos = 0;
      this.awaitPos = 0;
      // Do not save awaitIdentPos to allow checking awaits nested in parameters
      while (this.type !== types$1.parenR) {
        first ? first = false : this.expect(types$1.comma);
        if (allowTrailingComma && this.afterTrailingComma(types$1.parenR, true)) {
          lastIsComma = true;
          break
        } else if (this.type === types$1.ellipsis) {
          spreadStart = this.start;
          exprList.push(this.parseParenItem(this.parseRestBinding()));
          if (this.type === types$1.comma) {
            this.raiseRecoverable(
              this.start,
              "Comma is not permitted after the rest element"
            );
          }
          break
        } else {
          exprList.push(this.parseMaybeAssign(false, refDestructuringErrors, this.parseParenItem));
        }
      }
      var innerEndPos = this.lastTokEnd, innerEndLoc = this.lastTokEndLoc;
      this.expect(types$1.parenR);

      if (canBeArrow && this.shouldParseArrow(exprList) && this.eat(types$1.arrow)) {
        this.checkPatternErrors(refDestructuringErrors, false);
        this.checkYieldAwaitInDefaultParams();
        this.yieldPos = oldYieldPos;
        this.awaitPos = oldAwaitPos;
        return this.parseParenArrowList(startPos, startLoc, exprList, forInit)
      }

      if (!exprList.length || lastIsComma) { this.unexpected(this.lastTokStart); }
      if (spreadStart) { this.unexpected(spreadStart); }
      this.checkExpressionErrors(refDestructuringErrors, true);
      this.yieldPos = oldYieldPos || this.yieldPos;
      this.awaitPos = oldAwaitPos || this.awaitPos;

      if (exprList.length > 1) {
        val = this.startNodeAt(innerStartPos, innerStartLoc);
        val.expressions = exprList;
        this.finishNodeAt(val, "SequenceExpression", innerEndPos, innerEndLoc);
      } else {
        val = exprList[0];
      }
    } else {
      val = this.parseParenExpression();
    }

    if (this.options.preserveParens) {
      var par = this.startNodeAt(startPos, startLoc);
      par.expression = val;
      return this.finishNode(par, "ParenthesizedExpression")
    } else {
      return val
    }
  };

  pp$5.parseParenItem = function(item) {
    return item
  };

  pp$5.parseParenArrowList = function(startPos, startLoc, exprList, forInit) {
    return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), exprList, false, forInit)
  };

  // New's precedence is slightly tricky. It must allow its argument to
  // be a `[]` or dot subscript expression, but not a call — at least,
  // not without wrapping it in parentheses. Thus, it uses the noCalls
  // argument to parseSubscripts to prevent it from consuming the
  // argument list.

  var empty = [];

  pp$5.parseNew = function() {
    if (this.containsEsc) { this.raiseRecoverable(this.start, "Escape sequence in keyword new"); }
    var node = this.startNode();
    this.next();
    if (this.options.ecmaVersion >= 6 && this.type === types$1.dot) {
      var meta = this.startNodeAt(node.start, node.loc && node.loc.start);
      meta.name = "new";
      node.meta = this.finishNode(meta, "Identifier");
      this.next();
      var containsEsc = this.containsEsc;
      node.property = this.parseIdent(true);
      if (node.property.name !== "target")
        { this.raiseRecoverable(node.property.start, "The only valid meta property for new is 'new.target'"); }
      if (containsEsc)
        { this.raiseRecoverable(node.start, "'new.target' must not contain escaped characters"); }
      if (!this.allowNewDotTarget)
        { this.raiseRecoverable(node.start, "'new.target' can only be used in functions and class static block"); }
      return this.finishNode(node, "MetaProperty")
    }
    var startPos = this.start, startLoc = this.startLoc;
    node.callee = this.parseSubscripts(this.parseExprAtom(null, false, true), startPos, startLoc, true, false);
    if (this.eat(types$1.parenL)) { node.arguments = this.parseExprList(types$1.parenR, this.options.ecmaVersion >= 8, false); }
    else { node.arguments = empty; }
    return this.finishNode(node, "NewExpression")
  };

  // Parse template expression.

  pp$5.parseTemplateElement = function(ref) {
    var isTagged = ref.isTagged;

    var elem = this.startNode();
    if (this.type === types$1.invalidTemplate) {
      if (!isTagged) {
        this.raiseRecoverable(this.start, "Bad escape sequence in untagged template literal");
      }
      elem.value = {
        raw: this.value.replace(/\r\n?/g, "\n"),
        cooked: null
      };
    } else {
      elem.value = {
        raw: this.input.slice(this.start, this.end).replace(/\r\n?/g, "\n"),
        cooked: this.value
      };
    }
    this.next();
    elem.tail = this.type === types$1.backQuote;
    return this.finishNode(elem, "TemplateElement")
  };

  pp$5.parseTemplate = function(ref) {
    if ( ref === void 0 ) ref = {};
    var isTagged = ref.isTagged; if ( isTagged === void 0 ) isTagged = false;

    var node = this.startNode();
    this.next();
    node.expressions = [];
    var curElt = this.parseTemplateElement({isTagged: isTagged});
    node.quasis = [curElt];
    while (!curElt.tail) {
      if (this.type === types$1.eof) { this.raise(this.pos, "Unterminated template literal"); }
      this.expect(types$1.dollarBraceL);
      node.expressions.push(this.parseExpression());
      this.expect(types$1.braceR);
      node.quasis.push(curElt = this.parseTemplateElement({isTagged: isTagged}));
    }
    this.next();
    return this.finishNode(node, "TemplateLiteral")
  };

  pp$5.isAsyncProp = function(prop) {
    return !prop.computed && prop.key.type === "Identifier" && prop.key.name === "async" &&
      (this.type === types$1.name || this.type === types$1.num || this.type === types$1.string || this.type === types$1.bracketL || this.type.keyword || (this.options.ecmaVersion >= 9 && this.type === types$1.star)) &&
      !lineBreak.test(this.input.slice(this.lastTokEnd, this.start))
  };

  // Parse an object literal or binding pattern.

  pp$5.parseObj = function(isPattern, refDestructuringErrors) {
    var node = this.startNode(), first = true, propHash = {};
    node.properties = [];
    this.next();
    while (!this.eat(types$1.braceR)) {
      if (!first) {
        this.expect(types$1.comma);
        if (this.options.ecmaVersion >= 5 && this.afterTrailingComma(types$1.braceR)) { break }
      } else { first = false; }

      var prop = this.parseProperty(isPattern, refDestructuringErrors);
      if (!isPattern) { this.checkPropClash(prop, propHash, refDestructuringErrors); }
      node.properties.push(prop);
    }
    return this.finishNode(node, isPattern ? "ObjectPattern" : "ObjectExpression")
  };

  pp$5.parseProperty = function(isPattern, refDestructuringErrors) {
    var prop = this.startNode(), isGenerator, isAsync, startPos, startLoc;
    if (this.options.ecmaVersion >= 9 && this.eat(types$1.ellipsis)) {
      if (isPattern) {
        prop.argument = this.parseIdent(false);
        if (this.type === types$1.comma) {
          this.raiseRecoverable(this.start, "Comma is not permitted after the rest element");
        }
        return this.finishNode(prop, "RestElement")
      }
      // Parse argument.
      prop.argument = this.parseMaybeAssign(false, refDestructuringErrors);
      // To disallow trailing comma via `this.toAssignable()`.
      if (this.type === types$1.comma && refDestructuringErrors && refDestructuringErrors.trailingComma < 0) {
        refDestructuringErrors.trailingComma = this.start;
      }
      // Finish
      return this.finishNode(prop, "SpreadElement")
    }
    if (this.options.ecmaVersion >= 6) {
      prop.method = false;
      prop.shorthand = false;
      if (isPattern || refDestructuringErrors) {
        startPos = this.start;
        startLoc = this.startLoc;
      }
      if (!isPattern)
        { isGenerator = this.eat(types$1.star); }
    }
    var containsEsc = this.containsEsc;
    this.parsePropertyName(prop);
    if (!isPattern && !containsEsc && this.options.ecmaVersion >= 8 && !isGenerator && this.isAsyncProp(prop)) {
      isAsync = true;
      isGenerator = this.options.ecmaVersion >= 9 && this.eat(types$1.star);
      this.parsePropertyName(prop);
    } else {
      isAsync = false;
    }
    this.parsePropertyValue(prop, isPattern, isGenerator, isAsync, startPos, startLoc, refDestructuringErrors, containsEsc);
    return this.finishNode(prop, "Property")
  };

  pp$5.parseGetterSetter = function(prop) {
    var kind = prop.key.name;
    this.parsePropertyName(prop);
    prop.value = this.parseMethod(false);
    prop.kind = kind;
    var paramCount = prop.kind === "get" ? 0 : 1;
    if (prop.value.params.length !== paramCount) {
      var start = prop.value.start;
      if (prop.kind === "get")
        { this.raiseRecoverable(start, "getter should have no params"); }
      else
        { this.raiseRecoverable(start, "setter should have exactly one param"); }
    } else {
      if (prop.kind === "set" && prop.value.params[0].type === "RestElement")
        { this.raiseRecoverable(prop.value.params[0].start, "Setter cannot use rest params"); }
    }
  };

  pp$5.parsePropertyValue = function(prop, isPattern, isGenerator, isAsync, startPos, startLoc, refDestructuringErrors, containsEsc) {
    if ((isGenerator || isAsync) && this.type === types$1.colon)
      { this.unexpected(); }

    if (this.eat(types$1.colon)) {
      prop.value = isPattern ? this.parseMaybeDefault(this.start, this.startLoc) : this.parseMaybeAssign(false, refDestructuringErrors);
      prop.kind = "init";
    } else if (this.options.ecmaVersion >= 6 && this.type === types$1.parenL) {
      if (isPattern) { this.unexpected(); }
      prop.method = true;
      prop.value = this.parseMethod(isGenerator, isAsync);
      prop.kind = "init";
    } else if (!isPattern && !containsEsc &&
               this.options.ecmaVersion >= 5 && !prop.computed && prop.key.type === "Identifier" &&
               (prop.key.name === "get" || prop.key.name === "set") &&
               (this.type !== types$1.comma && this.type !== types$1.braceR && this.type !== types$1.eq)) {
      if (isGenerator || isAsync) { this.unexpected(); }
      this.parseGetterSetter(prop);
    } else if (this.options.ecmaVersion >= 6 && !prop.computed && prop.key.type === "Identifier") {
      if (isGenerator || isAsync) { this.unexpected(); }
      this.checkUnreserved(prop.key);
      if (prop.key.name === "await" && !this.awaitIdentPos)
        { this.awaitIdentPos = startPos; }
      if (isPattern) {
        prop.value = this.parseMaybeDefault(startPos, startLoc, this.copyNode(prop.key));
      } else if (this.type === types$1.eq && refDestructuringErrors) {
        if (refDestructuringErrors.shorthandAssign < 0)
          { refDestructuringErrors.shorthandAssign = this.start; }
        prop.value = this.parseMaybeDefault(startPos, startLoc, this.copyNode(prop.key));
      } else {
        prop.value = this.copyNode(prop.key);
      }
      prop.kind = "init";
      prop.shorthand = true;
    } else { this.unexpected(); }
  };

  pp$5.parsePropertyName = function(prop) {
    if (this.options.ecmaVersion >= 6) {
      if (this.eat(types$1.bracketL)) {
        prop.computed = true;
        prop.key = this.parseMaybeAssign();
        this.expect(types$1.bracketR);
        return prop.key
      } else {
        prop.computed = false;
      }
    }
    return prop.key = this.type === types$1.num || this.type === types$1.string ? this.parseExprAtom() : this.parseIdent(this.options.allowReserved !== "never")
  };

  // Initialize empty function node.

  pp$5.initFunction = function(node) {
    node.id = null;
    if (this.options.ecmaVersion >= 6) { node.generator = node.expression = false; }
    if (this.options.ecmaVersion >= 8) { node.async = false; }
  };

  // Parse object or class method.

  pp$5.parseMethod = function(isGenerator, isAsync, allowDirectSuper) {
    var node = this.startNode(), oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldAwaitIdentPos = this.awaitIdentPos;

    this.initFunction(node);
    if (this.options.ecmaVersion >= 6)
      { node.generator = isGenerator; }
    if (this.options.ecmaVersion >= 8)
      { node.async = !!isAsync; }

    this.yieldPos = 0;
    this.awaitPos = 0;
    this.awaitIdentPos = 0;
    this.enterScope(functionFlags(isAsync, node.generator) | SCOPE_SUPER | (allowDirectSuper ? SCOPE_DIRECT_SUPER : 0));

    this.expect(types$1.parenL);
    node.params = this.parseBindingList(types$1.parenR, false, this.options.ecmaVersion >= 8);
    this.checkYieldAwaitInDefaultParams();
    this.parseFunctionBody(node, false, true, false);

    this.yieldPos = oldYieldPos;
    this.awaitPos = oldAwaitPos;
    this.awaitIdentPos = oldAwaitIdentPos;
    return this.finishNode(node, "FunctionExpression")
  };

  // Parse arrow function expression with given parameters.

  pp$5.parseArrowExpression = function(node, params, isAsync, forInit) {
    var oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldAwaitIdentPos = this.awaitIdentPos;

    this.enterScope(functionFlags(isAsync, false) | SCOPE_ARROW);
    this.initFunction(node);
    if (this.options.ecmaVersion >= 8) { node.async = !!isAsync; }

    this.yieldPos = 0;
    this.awaitPos = 0;
    this.awaitIdentPos = 0;

    node.params = this.toAssignableList(params, true);
    this.parseFunctionBody(node, true, false, forInit);

    this.yieldPos = oldYieldPos;
    this.awaitPos = oldAwaitPos;
    this.awaitIdentPos = oldAwaitIdentPos;
    return this.finishNode(node, "ArrowFunctionExpression")
  };

  // Parse function body and check parameters.

  pp$5.parseFunctionBody = function(node, isArrowFunction, isMethod, forInit) {
    var isExpression = isArrowFunction && this.type !== types$1.braceL;
    var oldStrict = this.strict, useStrict = false;

    if (isExpression) {
      node.body = this.parseMaybeAssign(forInit);
      node.expression = true;
      this.checkParams(node, false);
    } else {
      var nonSimple = this.options.ecmaVersion >= 7 && !this.isSimpleParamList(node.params);
      if (!oldStrict || nonSimple) {
        useStrict = this.strictDirective(this.end);
        // If this is a strict mode function, verify that argument names
        // are not repeated, and it does not try to bind the words `eval`
        // or `arguments`.
        if (useStrict && nonSimple)
          { this.raiseRecoverable(node.start, "Illegal 'use strict' directive in function with non-simple parameter list"); }
      }
      // Start a new scope with regard to labels and the `inFunction`
      // flag (restore them to their old value afterwards).
      var oldLabels = this.labels;
      this.labels = [];
      if (useStrict) { this.strict = true; }

      // Add the params to varDeclaredNames to ensure that an error is thrown
      // if a let/const declaration in the function clashes with one of the params.
      this.checkParams(node, !oldStrict && !useStrict && !isArrowFunction && !isMethod && this.isSimpleParamList(node.params));
      // Ensure the function name isn't a forbidden identifier in strict mode, e.g. 'eval'
      if (this.strict && node.id) { this.checkLValSimple(node.id, BIND_OUTSIDE); }
      node.body = this.parseBlock(false, undefined, useStrict && !oldStrict);
      node.expression = false;
      this.adaptDirectivePrologue(node.body.body);
      this.labels = oldLabels;
    }
    this.exitScope();
  };

  pp$5.isSimpleParamList = function(params) {
    for (var i = 0, list = params; i < list.length; i += 1)
      {
      var param = list[i];

      if (param.type !== "Identifier") { return false
    } }
    return true
  };

  // Checks function params for various disallowed patterns such as using "eval"
  // or "arguments" and duplicate parameters.

  pp$5.checkParams = function(node, allowDuplicates) {
    var nameHash = Object.create(null);
    for (var i = 0, list = node.params; i < list.length; i += 1)
      {
      var param = list[i];

      this.checkLValInnerPattern(param, BIND_VAR, allowDuplicates ? null : nameHash);
    }
  };

  // Parses a comma-separated list of expressions, and returns them as
  // an array. `close` is the token type that ends the list, and
  // `allowEmpty` can be turned on to allow subsequent commas with
  // nothing in between them to be parsed as `null` (which is needed
  // for array literals).

  pp$5.parseExprList = function(close, allowTrailingComma, allowEmpty, refDestructuringErrors) {
    var elts = [], first = true;
    while (!this.eat(close)) {
      if (!first) {
        this.expect(types$1.comma);
        if (allowTrailingComma && this.afterTrailingComma(close)) { break }
      } else { first = false; }

      var elt = (void 0);
      if (allowEmpty && this.type === types$1.comma)
        { elt = null; }
      else if (this.type === types$1.ellipsis) {
        elt = this.parseSpread(refDestructuringErrors);
        if (refDestructuringErrors && this.type === types$1.comma && refDestructuringErrors.trailingComma < 0)
          { refDestructuringErrors.trailingComma = this.start; }
      } else {
        elt = this.parseMaybeAssign(false, refDestructuringErrors);
      }
      elts.push(elt);
    }
    return elts
  };

  pp$5.checkUnreserved = function(ref) {
    var start = ref.start;
    var end = ref.end;
    var name = ref.name;

    if (this.inGenerator && name === "yield")
      { this.raiseRecoverable(start, "Cannot use 'yield' as identifier inside a generator"); }
    if (this.inAsync && name === "await")
      { this.raiseRecoverable(start, "Cannot use 'await' as identifier inside an async function"); }
    if (!(this.currentThisScope().flags & SCOPE_VAR) && name === "arguments")
      { this.raiseRecoverable(start, "Cannot use 'arguments' in class field initializer"); }
    if (this.inClassStaticBlock && (name === "arguments" || name === "await"))
      { this.raise(start, ("Cannot use " + name + " in class static initialization block")); }
    if (this.keywords.test(name))
      { this.raise(start, ("Unexpected keyword '" + name + "'")); }
    if (this.options.ecmaVersion < 6 &&
      this.input.slice(start, end).indexOf("\\") !== -1) { return }
    var re = this.strict ? this.reservedWordsStrict : this.reservedWords;
    if (re.test(name)) {
      if (!this.inAsync && name === "await")
        { this.raiseRecoverable(start, "Cannot use keyword 'await' outside an async function"); }
      this.raiseRecoverable(start, ("The keyword '" + name + "' is reserved"));
    }
  };

  // Parse the next token as an identifier. If `liberal` is true (used
  // when parsing properties), it will also convert keywords into
  // identifiers.

  pp$5.parseIdent = function(liberal) {
    var node = this.parseIdentNode();
    this.next(!!liberal);
    this.finishNode(node, "Identifier");
    if (!liberal) {
      this.checkUnreserved(node);
      if (node.name === "await" && !this.awaitIdentPos)
        { this.awaitIdentPos = node.start; }
    }
    return node
  };

  pp$5.parseIdentNode = function() {
    var node = this.startNode();
    if (this.type === types$1.name) {
      node.name = this.value;
    } else if (this.type.keyword) {
      node.name = this.type.keyword;

      // To fix https://github.com/acornjs/acorn/issues/575
      // `class` and `function` keywords push new context into this.context.
      // But there is no chance to pop the context if the keyword is consumed as an identifier such as a property name.
      // If the previous token is a dot, this does not apply because the context-managing code already ignored the keyword
      if ((node.name === "class" || node.name === "function") &&
        (this.lastTokEnd !== this.lastTokStart + 1 || this.input.charCodeAt(this.lastTokStart) !== 46)) {
        this.context.pop();
      }
      this.type = types$1.name;
    } else {
      this.unexpected();
    }
    return node
  };

  pp$5.parsePrivateIdent = function() {
    var node = this.startNode();
    if (this.type === types$1.privateId) {
      node.name = this.value;
    } else {
      this.unexpected();
    }
    this.next();
    this.finishNode(node, "PrivateIdentifier");

    // For validating existence
    if (this.options.checkPrivateFields) {
      if (this.privateNameStack.length === 0) {
        this.raise(node.start, ("Private field '#" + (node.name) + "' must be declared in an enclosing class"));
      } else {
        this.privateNameStack[this.privateNameStack.length - 1].used.push(node);
      }
    }

    return node
  };

  // Parses yield expression inside generator.

  pp$5.parseYield = function(forInit) {
    if (!this.yieldPos) { this.yieldPos = this.start; }

    var node = this.startNode();
    this.next();
    if (this.type === types$1.semi || this.canInsertSemicolon() || (this.type !== types$1.star && !this.type.startsExpr)) {
      node.delegate = false;
      node.argument = null;
    } else {
      node.delegate = this.eat(types$1.star);
      node.argument = this.parseMaybeAssign(forInit);
    }
    return this.finishNode(node, "YieldExpression")
  };

  pp$5.parseAwait = function(forInit) {
    if (!this.awaitPos) { this.awaitPos = this.start; }

    var node = this.startNode();
    this.next();
    node.argument = this.parseMaybeUnary(null, true, false, forInit);
    return this.finishNode(node, "AwaitExpression")
  };

  var pp$4 = Parser.prototype;

  // This function is used to raise exceptions on parse errors. It
  // takes an offset integer (into the current `input`) to indicate
  // the location of the error, attaches the position to the end
  // of the error message, and then raises a `SyntaxError` with that
  // message.

  pp$4.raise = function(pos, message) {
    var loc = getLineInfo(this.input, pos);
    message += " (" + loc.line + ":" + loc.column + ")";
    if (this.sourceFile) {
      message += " in " + this.sourceFile;
    }
    var err = new SyntaxError(message);
    err.pos = pos; err.loc = loc; err.raisedAt = this.pos;
    throw err
  };

  pp$4.raiseRecoverable = pp$4.raise;

  pp$4.curPosition = function() {
    if (this.options.locations) {
      return new Position(this.curLine, this.pos - this.lineStart)
    }
  };

  var pp$3 = Parser.prototype;

  var Scope = function Scope(flags) {
    this.flags = flags;
    // A list of var-declared names in the current lexical scope
    this.var = [];
    // A list of lexically-declared names in the current lexical scope
    this.lexical = [];
    // A list of lexically-declared FunctionDeclaration names in the current lexical scope
    this.functions = [];
  };

  // The functions in this module keep track of declared variables in the current scope in order to detect duplicate variable names.

  pp$3.enterScope = function(flags) {
    this.scopeStack.push(new Scope(flags));
  };

  pp$3.exitScope = function() {
    this.scopeStack.pop();
  };

  // The spec says:
  // > At the top level of a function, or script, function declarations are
  // > treated like var declarations rather than like lexical declarations.
  pp$3.treatFunctionsAsVarInScope = function(scope) {
    return (scope.flags & SCOPE_FUNCTION) || !this.inModule && (scope.flags & SCOPE_TOP)
  };

  pp$3.declareName = function(name, bindingType, pos) {
    var redeclared = false;
    if (bindingType === BIND_LEXICAL) {
      var scope = this.currentScope();
      redeclared = scope.lexical.indexOf(name) > -1 || scope.functions.indexOf(name) > -1 || scope.var.indexOf(name) > -1;
      scope.lexical.push(name);
      if (this.inModule && (scope.flags & SCOPE_TOP))
        { delete this.undefinedExports[name]; }
    } else if (bindingType === BIND_SIMPLE_CATCH) {
      var scope$1 = this.currentScope();
      scope$1.lexical.push(name);
    } else if (bindingType === BIND_FUNCTION) {
      var scope$2 = this.currentScope();
      if (this.treatFunctionsAsVar)
        { redeclared = scope$2.lexical.indexOf(name) > -1; }
      else
        { redeclared = scope$2.lexical.indexOf(name) > -1 || scope$2.var.indexOf(name) > -1; }
      scope$2.functions.push(name);
    } else {
      for (var i = this.scopeStack.length - 1; i >= 0; --i) {
        var scope$3 = this.scopeStack[i];
        if (scope$3.lexical.indexOf(name) > -1 && !((scope$3.flags & SCOPE_SIMPLE_CATCH) && scope$3.lexical[0] === name) ||
            !this.treatFunctionsAsVarInScope(scope$3) && scope$3.functions.indexOf(name) > -1) {
          redeclared = true;
          break
        }
        scope$3.var.push(name);
        if (this.inModule && (scope$3.flags & SCOPE_TOP))
          { delete this.undefinedExports[name]; }
        if (scope$3.flags & SCOPE_VAR) { break }
      }
    }
    if (redeclared) { this.raiseRecoverable(pos, ("Identifier '" + name + "' has already been declared")); }
  };

  pp$3.checkLocalExport = function(id) {
    // scope.functions must be empty as Module code is always strict.
    if (this.scopeStack[0].lexical.indexOf(id.name) === -1 &&
        this.scopeStack[0].var.indexOf(id.name) === -1) {
      this.undefinedExports[id.name] = id;
    }
  };

  pp$3.currentScope = function() {
    return this.scopeStack[this.scopeStack.length - 1]
  };

  pp$3.currentVarScope = function() {
    for (var i = this.scopeStack.length - 1;; i--) {
      var scope = this.scopeStack[i];
      if (scope.flags & (SCOPE_VAR | SCOPE_CLASS_FIELD_INIT | SCOPE_CLASS_STATIC_BLOCK)) { return scope }
    }
  };

  // Could be useful for `this`, `new.target`, `super()`, `super.property`, and `super[property]`.
  pp$3.currentThisScope = function() {
    for (var i = this.scopeStack.length - 1;; i--) {
      var scope = this.scopeStack[i];
      if (scope.flags & (SCOPE_VAR | SCOPE_CLASS_FIELD_INIT | SCOPE_CLASS_STATIC_BLOCK) &&
          !(scope.flags & SCOPE_ARROW)) { return scope }
    }
  };

  var Node = function Node(parser, pos, loc) {
    this.type = "";
    this.start = pos;
    this.end = 0;
    if (parser.options.locations)
      { this.loc = new SourceLocation(parser, loc); }
    if (parser.options.directSourceFile)
      { this.sourceFile = parser.options.directSourceFile; }
    if (parser.options.ranges)
      { this.range = [pos, 0]; }
  };

  // Start an AST node, attaching a start offset.

  var pp$2 = Parser.prototype;

  pp$2.startNode = function() {
    return new Node(this, this.start, this.startLoc)
  };

  pp$2.startNodeAt = function(pos, loc) {
    return new Node(this, pos, loc)
  };

  // Finish an AST node, adding `type` and `end` properties.

  function finishNodeAt(node, type, pos, loc) {
    node.type = type;
    node.end = pos;
    if (this.options.locations)
      { node.loc.end = loc; }
    if (this.options.ranges)
      { node.range[1] = pos; }
    return node
  }

  pp$2.finishNode = function(node, type) {
    return finishNodeAt.call(this, node, type, this.lastTokEnd, this.lastTokEndLoc)
  };

  // Finish node at given position

  pp$2.finishNodeAt = function(node, type, pos, loc) {
    return finishNodeAt.call(this, node, type, pos, loc)
  };

  pp$2.copyNode = function(node) {
    var newNode = new Node(this, node.start, this.startLoc);
    for (var prop in node) { newNode[prop] = node[prop]; }
    return newNode
  };

  // This file was generated by "bin/generate-unicode-script-values.js". Do not modify manually!
  var scriptValuesAddedInUnicode = "Gara Garay Gukh Gurung_Khema Hrkt Katakana_Or_Hiragana Kawi Kirat_Rai Krai Nag_Mundari Nagm Ol_Onal Onao Sunu Sunuwar Todhri Todr Tulu_Tigalari Tutg Unknown Zzzz";

  // This file contains Unicode properties extracted from the ECMAScript specification.
  // The lists are extracted like so:
  // $$('#table-binary-unicode-properties > figure > table > tbody > tr > td:nth-child(1) code').map(el => el.innerText)

  // #table-binary-unicode-properties
  var ecma9BinaryProperties = "ASCII ASCII_Hex_Digit AHex Alphabetic Alpha Any Assigned Bidi_Control Bidi_C Bidi_Mirrored Bidi_M Case_Ignorable CI Cased Changes_When_Casefolded CWCF Changes_When_Casemapped CWCM Changes_When_Lowercased CWL Changes_When_NFKC_Casefolded CWKCF Changes_When_Titlecased CWT Changes_When_Uppercased CWU Dash Default_Ignorable_Code_Point DI Deprecated Dep Diacritic Dia Emoji Emoji_Component Emoji_Modifier Emoji_Modifier_Base Emoji_Presentation Extender Ext Grapheme_Base Gr_Base Grapheme_Extend Gr_Ext Hex_Digit Hex IDS_Binary_Operator IDSB IDS_Trinary_Operator IDST ID_Continue IDC ID_Start IDS Ideographic Ideo Join_Control Join_C Logical_Order_Exception LOE Lowercase Lower Math Noncharacter_Code_Point NChar Pattern_Syntax Pat_Syn Pattern_White_Space Pat_WS Quotation_Mark QMark Radical Regional_Indicator RI Sentence_Terminal STerm Soft_Dotted SD Terminal_Punctuation Term Unified_Ideograph UIdeo Uppercase Upper Variation_Selector VS White_Space space XID_Continue XIDC XID_Start XIDS";
  var ecma10BinaryProperties = ecma9BinaryProperties + " Extended_Pictographic";
  var ecma11BinaryProperties = ecma10BinaryProperties;
  var ecma12BinaryProperties = ecma11BinaryProperties + " EBase EComp EMod EPres ExtPict";
  var ecma13BinaryProperties = ecma12BinaryProperties;
  var ecma14BinaryProperties = ecma13BinaryProperties;

  var unicodeBinaryProperties = {
    9: ecma9BinaryProperties,
    10: ecma10BinaryProperties,
    11: ecma11BinaryProperties,
    12: ecma12BinaryProperties,
    13: ecma13BinaryProperties,
    14: ecma14BinaryProperties
  };

  // #table-binary-unicode-properties-of-strings
  var ecma14BinaryPropertiesOfStrings = "Basic_Emoji Emoji_Keycap_Sequence RGI_Emoji_Modifier_Sequence RGI_Emoji_Flag_Sequence RGI_Emoji_Tag_Sequence RGI_Emoji_ZWJ_Sequence RGI_Emoji";

  var unicodeBinaryPropertiesOfStrings = {
    9: "",
    10: "",
    11: "",
    12: "",
    13: "",
    14: ecma14BinaryPropertiesOfStrings
  };

  // #table-unicode-general-category-values
  var unicodeGeneralCategoryValues = "Cased_Letter LC Close_Punctuation Pe Connector_Punctuation Pc Control Cc cntrl Currency_Symbol Sc Dash_Punctuation Pd Decimal_Number Nd digit Enclosing_Mark Me Final_Punctuation Pf Format Cf Initial_Punctuation Pi Letter L Letter_Number Nl Line_Separator Zl Lowercase_Letter Ll Mark M Combining_Mark Math_Symbol Sm Modifier_Letter Lm Modifier_Symbol Sk Nonspacing_Mark Mn Number N Open_Punctuation Ps Other C Other_Letter Lo Other_Number No Other_Punctuation Po Other_Symbol So Paragraph_Separator Zp Private_Use Co Punctuation P punct Separator Z Space_Separator Zs Spacing_Mark Mc Surrogate Cs Symbol S Titlecase_Letter Lt Unassigned Cn Uppercase_Letter Lu";

  // #table-unicode-script-values
  var ecma9ScriptValues = "Adlam Adlm Ahom Anatolian_Hieroglyphs Hluw Arabic Arab Armenian Armn Avestan Avst Balinese Bali Bamum Bamu Bassa_Vah Bass Batak Batk Bengali Beng Bhaiksuki Bhks Bopomofo Bopo Brahmi Brah Braille Brai Buginese Bugi Buhid Buhd Canadian_Aboriginal Cans Carian Cari Caucasian_Albanian Aghb Chakma Cakm Cham Cham Cherokee Cher Common Zyyy Coptic Copt Qaac Cuneiform Xsux Cypriot Cprt Cyrillic Cyrl Deseret Dsrt Devanagari Deva Duployan Dupl Egyptian_Hieroglyphs Egyp Elbasan Elba Ethiopic Ethi Georgian Geor Glagolitic Glag Gothic Goth Grantha Gran Greek Grek Gujarati Gujr Gurmukhi Guru Han Hani Hangul Hang Hanunoo Hano Hatran Hatr Hebrew Hebr Hiragana Hira Imperial_Aramaic Armi Inherited Zinh Qaai Inscriptional_Pahlavi Phli Inscriptional_Parthian Prti Javanese Java Kaithi Kthi Kannada Knda Katakana Kana Kayah_Li Kali Kharoshthi Khar Khmer Khmr Khojki Khoj Khudawadi Sind Lao Laoo Latin Latn Lepcha Lepc Limbu Limb Linear_A Lina Linear_B Linb Lisu Lisu Lycian Lyci Lydian Lydi Mahajani Mahj Malayalam Mlym Mandaic Mand Manichaean Mani Marchen Marc Masaram_Gondi Gonm Meetei_Mayek Mtei Mende_Kikakui Mend Meroitic_Cursive Merc Meroitic_Hieroglyphs Mero Miao Plrd Modi Mongolian Mong Mro Mroo Multani Mult Myanmar Mymr Nabataean Nbat New_Tai_Lue Talu Newa Newa Nko Nkoo Nushu Nshu Ogham Ogam Ol_Chiki Olck Old_Hungarian Hung Old_Italic Ital Old_North_Arabian Narb Old_Permic Perm Old_Persian Xpeo Old_South_Arabian Sarb Old_Turkic Orkh Oriya Orya Osage Osge Osmanya Osma Pahawh_Hmong Hmng Palmyrene Palm Pau_Cin_Hau Pauc Phags_Pa Phag Phoenician Phnx Psalter_Pahlavi Phlp Rejang Rjng Runic Runr Samaritan Samr Saurashtra Saur Sharada Shrd Shavian Shaw Siddham Sidd SignWriting Sgnw Sinhala Sinh Sora_Sompeng Sora Soyombo Soyo Sundanese Sund Syloti_Nagri Sylo Syriac Syrc Tagalog Tglg Tagbanwa Tagb Tai_Le Tale Tai_Tham Lana Tai_Viet Tavt Takri Takr Tamil Taml Tangut Tang Telugu Telu Thaana Thaa Thai Thai Tibetan Tibt Tifinagh Tfng Tirhuta Tirh Ugaritic Ugar Vai Vaii Warang_Citi Wara Yi Yiii Zanabazar_Square Zanb";
  var ecma10ScriptValues = ecma9ScriptValues + " Dogra Dogr Gunjala_Gondi Gong Hanifi_Rohingya Rohg Makasar Maka Medefaidrin Medf Old_Sogdian Sogo Sogdian Sogd";
  var ecma11ScriptValues = ecma10ScriptValues + " Elymaic Elym Nandinagari Nand Nyiakeng_Puachue_Hmong Hmnp Wancho Wcho";
  var ecma12ScriptValues = ecma11ScriptValues + " Chorasmian Chrs Diak Dives_Akuru Khitan_Small_Script Kits Yezi Yezidi";
  var ecma13ScriptValues = ecma12ScriptValues + " Cypro_Minoan Cpmn Old_Uyghur Ougr Tangsa Tnsa Toto Vithkuqi Vith";
  var ecma14ScriptValues = ecma13ScriptValues + " " + scriptValuesAddedInUnicode;

  var unicodeScriptValues = {
    9: ecma9ScriptValues,
    10: ecma10ScriptValues,
    11: ecma11ScriptValues,
    12: ecma12ScriptValues,
    13: ecma13ScriptValues,
    14: ecma14ScriptValues
  };

  var data$1 = {};
  function buildUnicodeData(ecmaVersion) {
    var d = data$1[ecmaVersion] = {
      binary: wordsRegexp(unicodeBinaryProperties[ecmaVersion] + " " + unicodeGeneralCategoryValues),
      binaryOfStrings: wordsRegexp(unicodeBinaryPropertiesOfStrings[ecmaVersion]),
      nonBinary: {
        General_Category: wordsRegexp(unicodeGeneralCategoryValues),
        Script: wordsRegexp(unicodeScriptValues[ecmaVersion])
      }
    };
    d.nonBinary.Script_Extensions = d.nonBinary.Script;

    d.nonBinary.gc = d.nonBinary.General_Category;
    d.nonBinary.sc = d.nonBinary.Script;
    d.nonBinary.scx = d.nonBinary.Script_Extensions;
  }

  for (var i = 0, list = [9, 10, 11, 12, 13, 14]; i < list.length; i += 1) {
    var ecmaVersion = list[i];

    buildUnicodeData(ecmaVersion);
  }

  var pp$1 = Parser.prototype;

  // Track disjunction structure to determine whether a duplicate
  // capture group name is allowed because it is in a separate branch.
  var BranchID = function BranchID(parent, base) {
    // Parent disjunction branch
    this.parent = parent;
    // Identifies this set of sibling branches
    this.base = base || this;
  };

  BranchID.prototype.separatedFrom = function separatedFrom (alt) {
    // A branch is separate from another branch if they or any of
    // their parents are siblings in a given disjunction
    for (var self = this; self; self = self.parent) {
      for (var other = alt; other; other = other.parent) {
        if (self.base === other.base && self !== other) { return true }
      }
    }
    return false
  };

  BranchID.prototype.sibling = function sibling () {
    return new BranchID(this.parent, this.base)
  };

  var RegExpValidationState = function RegExpValidationState(parser) {
    this.parser = parser;
    this.validFlags = "gim" + (parser.options.ecmaVersion >= 6 ? "uy" : "") + (parser.options.ecmaVersion >= 9 ? "s" : "") + (parser.options.ecmaVersion >= 13 ? "d" : "") + (parser.options.ecmaVersion >= 15 ? "v" : "");
    this.unicodeProperties = data$1[parser.options.ecmaVersion >= 14 ? 14 : parser.options.ecmaVersion];
    this.source = "";
    this.flags = "";
    this.start = 0;
    this.switchU = false;
    this.switchV = false;
    this.switchN = false;
    this.pos = 0;
    this.lastIntValue = 0;
    this.lastStringValue = "";
    this.lastAssertionIsQuantifiable = false;
    this.numCapturingParens = 0;
    this.maxBackReference = 0;
    this.groupNames = Object.create(null);
    this.backReferenceNames = [];
    this.branchID = null;
  };

  RegExpValidationState.prototype.reset = function reset (start, pattern, flags) {
    var unicodeSets = flags.indexOf("v") !== -1;
    var unicode = flags.indexOf("u") !== -1;
    this.start = start | 0;
    this.source = pattern + "";
    this.flags = flags;
    if (unicodeSets && this.parser.options.ecmaVersion >= 15) {
      this.switchU = true;
      this.switchV = true;
      this.switchN = true;
    } else {
      this.switchU = unicode && this.parser.options.ecmaVersion >= 6;
      this.switchV = false;
      this.switchN = unicode && this.parser.options.ecmaVersion >= 9;
    }
  };

  RegExpValidationState.prototype.raise = function raise (message) {
    this.parser.raiseRecoverable(this.start, ("Invalid regular expression: /" + (this.source) + "/: " + message));
  };

  // If u flag is given, this returns the code point at the index (it combines a surrogate pair).
  // Otherwise, this returns the code unit of the index (can be a part of a surrogate pair).
  RegExpValidationState.prototype.at = function at (i, forceU) {
      if ( forceU === void 0 ) forceU = false;

    var s = this.source;
    var l = s.length;
    if (i >= l) {
      return -1
    }
    var c = s.charCodeAt(i);
    if (!(forceU || this.switchU) || c <= 0xD7FF || c >= 0xE000 || i + 1 >= l) {
      return c
    }
    var next = s.charCodeAt(i + 1);
    return next >= 0xDC00 && next <= 0xDFFF ? (c << 10) + next - 0x35FDC00 : c
  };

  RegExpValidationState.prototype.nextIndex = function nextIndex (i, forceU) {
      if ( forceU === void 0 ) forceU = false;

    var s = this.source;
    var l = s.length;
    if (i >= l) {
      return l
    }
    var c = s.charCodeAt(i), next;
    if (!(forceU || this.switchU) || c <= 0xD7FF || c >= 0xE000 || i + 1 >= l ||
        (next = s.charCodeAt(i + 1)) < 0xDC00 || next > 0xDFFF) {
      return i + 1
    }
    return i + 2
  };

  RegExpValidationState.prototype.current = function current (forceU) {
      if ( forceU === void 0 ) forceU = false;

    return this.at(this.pos, forceU)
  };

  RegExpValidationState.prototype.lookahead = function lookahead (forceU) {
      if ( forceU === void 0 ) forceU = false;

    return this.at(this.nextIndex(this.pos, forceU), forceU)
  };

  RegExpValidationState.prototype.advance = function advance (forceU) {
      if ( forceU === void 0 ) forceU = false;

    this.pos = this.nextIndex(this.pos, forceU);
  };

  RegExpValidationState.prototype.eat = function eat (ch, forceU) {
      if ( forceU === void 0 ) forceU = false;

    if (this.current(forceU) === ch) {
      this.advance(forceU);
      return true
    }
    return false
  };

  RegExpValidationState.prototype.eatChars = function eatChars (chs, forceU) {
      if ( forceU === void 0 ) forceU = false;

    var pos = this.pos;
    for (var i = 0, list = chs; i < list.length; i += 1) {
      var ch = list[i];

        var current = this.at(pos, forceU);
      if (current === -1 || current !== ch) {
        return false
      }
      pos = this.nextIndex(pos, forceU);
    }
    this.pos = pos;
    return true
  };

  /**
   * Validate the flags part of a given RegExpLiteral.
   *
   * @param {RegExpValidationState} state The state to validate RegExp.
   * @returns {void}
   */
  pp$1.validateRegExpFlags = function(state) {
    var validFlags = state.validFlags;
    var flags = state.flags;

    var u = false;
    var v = false;

    for (var i = 0; i < flags.length; i++) {
      var flag = flags.charAt(i);
      if (validFlags.indexOf(flag) === -1) {
        this.raise(state.start, "Invalid regular expression flag");
      }
      if (flags.indexOf(flag, i + 1) > -1) {
        this.raise(state.start, "Duplicate regular expression flag");
      }
      if (flag === "u") { u = true; }
      if (flag === "v") { v = true; }
    }
    if (this.options.ecmaVersion >= 15 && u && v) {
      this.raise(state.start, "Invalid regular expression flag");
    }
  };

  function hasProp(obj) {
    for (var _ in obj) { return true }
    return false
  }

  /**
   * Validate the pattern part of a given RegExpLiteral.
   *
   * @param {RegExpValidationState} state The state to validate RegExp.
   * @returns {void}
   */
  pp$1.validateRegExpPattern = function(state) {
    this.regexp_pattern(state);

    // The goal symbol for the parse is |Pattern[~U, ~N]|. If the result of
    // parsing contains a |GroupName|, reparse with the goal symbol
    // |Pattern[~U, +N]| and use this result instead. Throw a *SyntaxError*
    // exception if _P_ did not conform to the grammar, if any elements of _P_
    // were not matched by the parse, or if any Early Error conditions exist.
    if (!state.switchN && this.options.ecmaVersion >= 9 && hasProp(state.groupNames)) {
      state.switchN = true;
      this.regexp_pattern(state);
    }
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-Pattern
  pp$1.regexp_pattern = function(state) {
    state.pos = 0;
    state.lastIntValue = 0;
    state.lastStringValue = "";
    state.lastAssertionIsQuantifiable = false;
    state.numCapturingParens = 0;
    state.maxBackReference = 0;
    state.groupNames = Object.create(null);
    state.backReferenceNames.length = 0;
    state.branchID = null;

    this.regexp_disjunction(state);

    if (state.pos !== state.source.length) {
      // Make the same messages as V8.
      if (state.eat(0x29 /* ) */)) {
        state.raise("Unmatched ')'");
      }
      if (state.eat(0x5D /* ] */) || state.eat(0x7D /* } */)) {
        state.raise("Lone quantifier brackets");
      }
    }
    if (state.maxBackReference > state.numCapturingParens) {
      state.raise("Invalid escape");
    }
    for (var i = 0, list = state.backReferenceNames; i < list.length; i += 1) {
      var name = list[i];

      if (!state.groupNames[name]) {
        state.raise("Invalid named capture referenced");
      }
    }
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-Disjunction
  pp$1.regexp_disjunction = function(state) {
    var trackDisjunction = this.options.ecmaVersion >= 16;
    if (trackDisjunction) { state.branchID = new BranchID(state.branchID, null); }
    this.regexp_alternative(state);
    while (state.eat(0x7C /* | */)) {
      if (trackDisjunction) { state.branchID = state.branchID.sibling(); }
      this.regexp_alternative(state);
    }
    if (trackDisjunction) { state.branchID = state.branchID.parent; }

    // Make the same message as V8.
    if (this.regexp_eatQuantifier(state, true)) {
      state.raise("Nothing to repeat");
    }
    if (state.eat(0x7B /* { */)) {
      state.raise("Lone quantifier brackets");
    }
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-Alternative
  pp$1.regexp_alternative = function(state) {
    while (state.pos < state.source.length && this.regexp_eatTerm(state)) {}
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-Term
  pp$1.regexp_eatTerm = function(state) {
    if (this.regexp_eatAssertion(state)) {
      // Handle `QuantifiableAssertion Quantifier` alternative.
      // `state.lastAssertionIsQuantifiable` is true if the last eaten Assertion
      // is a QuantifiableAssertion.
      if (state.lastAssertionIsQuantifiable && this.regexp_eatQuantifier(state)) {
        // Make the same message as V8.
        if (state.switchU) {
          state.raise("Invalid quantifier");
        }
      }
      return true
    }

    if (state.switchU ? this.regexp_eatAtom(state) : this.regexp_eatExtendedAtom(state)) {
      this.regexp_eatQuantifier(state);
      return true
    }

    return false
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-Assertion
  pp$1.regexp_eatAssertion = function(state) {
    var start = state.pos;
    state.lastAssertionIsQuantifiable = false;

    // ^, $
    if (state.eat(0x5E /* ^ */) || state.eat(0x24 /* $ */)) {
      return true
    }

    // \b \B
    if (state.eat(0x5C /* \ */)) {
      if (state.eat(0x42 /* B */) || state.eat(0x62 /* b */)) {
        return true
      }
      state.pos = start;
    }

    // Lookahead / Lookbehind
    if (state.eat(0x28 /* ( */) && state.eat(0x3F /* ? */)) {
      var lookbehind = false;
      if (this.options.ecmaVersion >= 9) {
        lookbehind = state.eat(0x3C /* < */);
      }
      if (state.eat(0x3D /* = */) || state.eat(0x21 /* ! */)) {
        this.regexp_disjunction(state);
        if (!state.eat(0x29 /* ) */)) {
          state.raise("Unterminated group");
        }
        state.lastAssertionIsQuantifiable = !lookbehind;
        return true
      }
    }

    state.pos = start;
    return false
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-Quantifier
  pp$1.regexp_eatQuantifier = function(state, noError) {
    if ( noError === void 0 ) noError = false;

    if (this.regexp_eatQuantifierPrefix(state, noError)) {
      state.eat(0x3F /* ? */);
      return true
    }
    return false
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-QuantifierPrefix
  pp$1.regexp_eatQuantifierPrefix = function(state, noError) {
    return (
      state.eat(0x2A /* * */) ||
      state.eat(0x2B /* + */) ||
      state.eat(0x3F /* ? */) ||
      this.regexp_eatBracedQuantifier(state, noError)
    )
  };
  pp$1.regexp_eatBracedQuantifier = function(state, noError) {
    var start = state.pos;
    if (state.eat(0x7B /* { */)) {
      var min = 0, max = -1;
      if (this.regexp_eatDecimalDigits(state)) {
        min = state.lastIntValue;
        if (state.eat(0x2C /* , */) && this.regexp_eatDecimalDigits(state)) {
          max = state.lastIntValue;
        }
        if (state.eat(0x7D /* } */)) {
          // SyntaxError in https://www.ecma-international.org/ecma-262/8.0/#sec-term
          if (max !== -1 && max < min && !noError) {
            state.raise("numbers out of order in {} quantifier");
          }
          return true
        }
      }
      if (state.switchU && !noError) {
        state.raise("Incomplete quantifier");
      }
      state.pos = start;
    }
    return false
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-Atom
  pp$1.regexp_eatAtom = function(state) {
    return (
      this.regexp_eatPatternCharacters(state) ||
      state.eat(0x2E /* . */) ||
      this.regexp_eatReverseSolidusAtomEscape(state) ||
      this.regexp_eatCharacterClass(state) ||
      this.regexp_eatUncapturingGroup(state) ||
      this.regexp_eatCapturingGroup(state)
    )
  };
  pp$1.regexp_eatReverseSolidusAtomEscape = function(state) {
    var start = state.pos;
    if (state.eat(0x5C /* \ */)) {
      if (this.regexp_eatAtomEscape(state)) {
        return true
      }
      state.pos = start;
    }
    return false
  };
  pp$1.regexp_eatUncapturingGroup = function(state) {
    var start = state.pos;
    if (state.eat(0x28 /* ( */)) {
      if (state.eat(0x3F /* ? */)) {
        if (this.options.ecmaVersion >= 16) {
          var addModifiers = this.regexp_eatModifiers(state);
          var hasHyphen = state.eat(0x2D /* - */);
          if (addModifiers || hasHyphen) {
            for (var i = 0; i < addModifiers.length; i++) {
              var modifier = addModifiers.charAt(i);
              if (addModifiers.indexOf(modifier, i + 1) > -1) {
                state.raise("Duplicate regular expression modifiers");
              }
            }
            if (hasHyphen) {
              var removeModifiers = this.regexp_eatModifiers(state);
              if (!addModifiers && !removeModifiers && state.current() === 0x3A /* : */) {
                state.raise("Invalid regular expression modifiers");
              }
              for (var i$1 = 0; i$1 < removeModifiers.length; i$1++) {
                var modifier$1 = removeModifiers.charAt(i$1);
                if (
                  removeModifiers.indexOf(modifier$1, i$1 + 1) > -1 ||
                  addModifiers.indexOf(modifier$1) > -1
                ) {
                  state.raise("Duplicate regular expression modifiers");
                }
              }
            }
          }
        }
        if (state.eat(0x3A /* : */)) {
          this.regexp_disjunction(state);
          if (state.eat(0x29 /* ) */)) {
            return true
          }
          state.raise("Unterminated group");
        }
      }
      state.pos = start;
    }
    return false
  };
  pp$1.regexp_eatCapturingGroup = function(state) {
    if (state.eat(0x28 /* ( */)) {
      if (this.options.ecmaVersion >= 9) {
        this.regexp_groupSpecifier(state);
      } else if (state.current() === 0x3F /* ? */) {
        state.raise("Invalid group");
      }
      this.regexp_disjunction(state);
      if (state.eat(0x29 /* ) */)) {
        state.numCapturingParens += 1;
        return true
      }
      state.raise("Unterminated group");
    }
    return false
  };
  // RegularExpressionModifiers ::
  //   [empty]
  //   RegularExpressionModifiers RegularExpressionModifier
  pp$1.regexp_eatModifiers = function(state) {
    var modifiers = "";
    var ch = 0;
    while ((ch = state.current()) !== -1 && isRegularExpressionModifier(ch)) {
      modifiers += codePointToString(ch);
      state.advance();
    }
    return modifiers
  };
  // RegularExpressionModifier :: one of
  //   `i` `m` `s`
  function isRegularExpressionModifier(ch) {
    return ch === 0x69 /* i */ || ch === 0x6d /* m */ || ch === 0x73 /* s */
  }

  // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-ExtendedAtom
  pp$1.regexp_eatExtendedAtom = function(state) {
    return (
      state.eat(0x2E /* . */) ||
      this.regexp_eatReverseSolidusAtomEscape(state) ||
      this.regexp_eatCharacterClass(state) ||
      this.regexp_eatUncapturingGroup(state) ||
      this.regexp_eatCapturingGroup(state) ||
      this.regexp_eatInvalidBracedQuantifier(state) ||
      this.regexp_eatExtendedPatternCharacter(state)
    )
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-InvalidBracedQuantifier
  pp$1.regexp_eatInvalidBracedQuantifier = function(state) {
    if (this.regexp_eatBracedQuantifier(state, true)) {
      state.raise("Nothing to repeat");
    }
    return false
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-SyntaxCharacter
  pp$1.regexp_eatSyntaxCharacter = function(state) {
    var ch = state.current();
    if (isSyntaxCharacter(ch)) {
      state.lastIntValue = ch;
      state.advance();
      return true
    }
    return false
  };
  function isSyntaxCharacter(ch) {
    return (
      ch === 0x24 /* $ */ ||
      ch >= 0x28 /* ( */ && ch <= 0x2B /* + */ ||
      ch === 0x2E /* . */ ||
      ch === 0x3F /* ? */ ||
      ch >= 0x5B /* [ */ && ch <= 0x5E /* ^ */ ||
      ch >= 0x7B /* { */ && ch <= 0x7D /* } */
    )
  }

  // https://www.ecma-international.org/ecma-262/8.0/#prod-PatternCharacter
  // But eat eager.
  pp$1.regexp_eatPatternCharacters = function(state) {
    var start = state.pos;
    var ch = 0;
    while ((ch = state.current()) !== -1 && !isSyntaxCharacter(ch)) {
      state.advance();
    }
    return state.pos !== start
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-ExtendedPatternCharacter
  pp$1.regexp_eatExtendedPatternCharacter = function(state) {
    var ch = state.current();
    if (
      ch !== -1 &&
      ch !== 0x24 /* $ */ &&
      !(ch >= 0x28 /* ( */ && ch <= 0x2B /* + */) &&
      ch !== 0x2E /* . */ &&
      ch !== 0x3F /* ? */ &&
      ch !== 0x5B /* [ */ &&
      ch !== 0x5E /* ^ */ &&
      ch !== 0x7C /* | */
    ) {
      state.advance();
      return true
    }
    return false
  };

  // GroupSpecifier ::
  //   [empty]
  //   `?` GroupName
  pp$1.regexp_groupSpecifier = function(state) {
    if (state.eat(0x3F /* ? */)) {
      if (!this.regexp_eatGroupName(state)) { state.raise("Invalid group"); }
      var trackDisjunction = this.options.ecmaVersion >= 16;
      var known = state.groupNames[state.lastStringValue];
      if (known) {
        if (trackDisjunction) {
          for (var i = 0, list = known; i < list.length; i += 1) {
            var altID = list[i];

            if (!altID.separatedFrom(state.branchID))
              { state.raise("Duplicate capture group name"); }
          }
        } else {
          state.raise("Duplicate capture group name");
        }
      }
      if (trackDisjunction) {
        (known || (state.groupNames[state.lastStringValue] = [])).push(state.branchID);
      } else {
        state.groupNames[state.lastStringValue] = true;
      }
    }
  };

  // GroupName ::
  //   `<` RegExpIdentifierName `>`
  // Note: this updates `state.lastStringValue` property with the eaten name.
  pp$1.regexp_eatGroupName = function(state) {
    state.lastStringValue = "";
    if (state.eat(0x3C /* < */)) {
      if (this.regexp_eatRegExpIdentifierName(state) && state.eat(0x3E /* > */)) {
        return true
      }
      state.raise("Invalid capture group name");
    }
    return false
  };

  // RegExpIdentifierName ::
  //   RegExpIdentifierStart
  //   RegExpIdentifierName RegExpIdentifierPart
  // Note: this updates `state.lastStringValue` property with the eaten name.
  pp$1.regexp_eatRegExpIdentifierName = function(state) {
    state.lastStringValue = "";
    if (this.regexp_eatRegExpIdentifierStart(state)) {
      state.lastStringValue += codePointToString(state.lastIntValue);
      while (this.regexp_eatRegExpIdentifierPart(state)) {
        state.lastStringValue += codePointToString(state.lastIntValue);
      }
      return true
    }
    return false
  };

  // RegExpIdentifierStart ::
  //   UnicodeIDStart
  //   `$`
  //   `_`
  //   `\` RegExpUnicodeEscapeSequence[+U]
  pp$1.regexp_eatRegExpIdentifierStart = function(state) {
    var start = state.pos;
    var forceU = this.options.ecmaVersion >= 11;
    var ch = state.current(forceU);
    state.advance(forceU);

    if (ch === 0x5C /* \ */ && this.regexp_eatRegExpUnicodeEscapeSequence(state, forceU)) {
      ch = state.lastIntValue;
    }
    if (isRegExpIdentifierStart(ch)) {
      state.lastIntValue = ch;
      return true
    }

    state.pos = start;
    return false
  };
  function isRegExpIdentifierStart(ch) {
    return isIdentifierStart(ch, true) || ch === 0x24 /* $ */ || ch === 0x5F /* _ */
  }

  // RegExpIdentifierPart ::
  //   UnicodeIDContinue
  //   `$`
  //   `_`
  //   `\` RegExpUnicodeEscapeSequence[+U]
  //   <ZWNJ>
  //   <ZWJ>
  pp$1.regexp_eatRegExpIdentifierPart = function(state) {
    var start = state.pos;
    var forceU = this.options.ecmaVersion >= 11;
    var ch = state.current(forceU);
    state.advance(forceU);

    if (ch === 0x5C /* \ */ && this.regexp_eatRegExpUnicodeEscapeSequence(state, forceU)) {
      ch = state.lastIntValue;
    }
    if (isRegExpIdentifierPart(ch)) {
      state.lastIntValue = ch;
      return true
    }

    state.pos = start;
    return false
  };
  function isRegExpIdentifierPart(ch) {
    return isIdentifierChar(ch, true) || ch === 0x24 /* $ */ || ch === 0x5F /* _ */ || ch === 0x200C /* <ZWNJ> */ || ch === 0x200D /* <ZWJ> */
  }

  // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-AtomEscape
  pp$1.regexp_eatAtomEscape = function(state) {
    if (
      this.regexp_eatBackReference(state) ||
      this.regexp_eatCharacterClassEscape(state) ||
      this.regexp_eatCharacterEscape(state) ||
      (state.switchN && this.regexp_eatKGroupName(state))
    ) {
      return true
    }
    if (state.switchU) {
      // Make the same message as V8.
      if (state.current() === 0x63 /* c */) {
        state.raise("Invalid unicode escape");
      }
      state.raise("Invalid escape");
    }
    return false
  };
  pp$1.regexp_eatBackReference = function(state) {
    var start = state.pos;
    if (this.regexp_eatDecimalEscape(state)) {
      var n = state.lastIntValue;
      if (state.switchU) {
        // For SyntaxError in https://www.ecma-international.org/ecma-262/8.0/#sec-atomescape
        if (n > state.maxBackReference) {
          state.maxBackReference = n;
        }
        return true
      }
      if (n <= state.numCapturingParens) {
        return true
      }
      state.pos = start;
    }
    return false
  };
  pp$1.regexp_eatKGroupName = function(state) {
    if (state.eat(0x6B /* k */)) {
      if (this.regexp_eatGroupName(state)) {
        state.backReferenceNames.push(state.lastStringValue);
        return true
      }
      state.raise("Invalid named reference");
    }
    return false
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-CharacterEscape
  pp$1.regexp_eatCharacterEscape = function(state) {
    return (
      this.regexp_eatControlEscape(state) ||
      this.regexp_eatCControlLetter(state) ||
      this.regexp_eatZero(state) ||
      this.regexp_eatHexEscapeSequence(state) ||
      this.regexp_eatRegExpUnicodeEscapeSequence(state, false) ||
      (!state.switchU && this.regexp_eatLegacyOctalEscapeSequence(state)) ||
      this.regexp_eatIdentityEscape(state)
    )
  };
  pp$1.regexp_eatCControlLetter = function(state) {
    var start = state.pos;
    if (state.eat(0x63 /* c */)) {
      if (this.regexp_eatControlLetter(state)) {
        return true
      }
      state.pos = start;
    }
    return false
  };
  pp$1.regexp_eatZero = function(state) {
    if (state.current() === 0x30 /* 0 */ && !isDecimalDigit(state.lookahead())) {
      state.lastIntValue = 0;
      state.advance();
      return true
    }
    return false
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-ControlEscape
  pp$1.regexp_eatControlEscape = function(state) {
    var ch = state.current();
    if (ch === 0x74 /* t */) {
      state.lastIntValue = 0x09; /* \t */
      state.advance();
      return true
    }
    if (ch === 0x6E /* n */) {
      state.lastIntValue = 0x0A; /* \n */
      state.advance();
      return true
    }
    if (ch === 0x76 /* v */) {
      state.lastIntValue = 0x0B; /* \v */
      state.advance();
      return true
    }
    if (ch === 0x66 /* f */) {
      state.lastIntValue = 0x0C; /* \f */
      state.advance();
      return true
    }
    if (ch === 0x72 /* r */) {
      state.lastIntValue = 0x0D; /* \r */
      state.advance();
      return true
    }
    return false
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-ControlLetter
  pp$1.regexp_eatControlLetter = function(state) {
    var ch = state.current();
    if (isControlLetter(ch)) {
      state.lastIntValue = ch % 0x20;
      state.advance();
      return true
    }
    return false
  };
  function isControlLetter(ch) {
    return (
      (ch >= 0x41 /* A */ && ch <= 0x5A /* Z */) ||
      (ch >= 0x61 /* a */ && ch <= 0x7A /* z */)
    )
  }

  // https://www.ecma-international.org/ecma-262/8.0/#prod-RegExpUnicodeEscapeSequence
  pp$1.regexp_eatRegExpUnicodeEscapeSequence = function(state, forceU) {
    if ( forceU === void 0 ) forceU = false;

    var start = state.pos;
    var switchU = forceU || state.switchU;

    if (state.eat(0x75 /* u */)) {
      if (this.regexp_eatFixedHexDigits(state, 4)) {
        var lead = state.lastIntValue;
        if (switchU && lead >= 0xD800 && lead <= 0xDBFF) {
          var leadSurrogateEnd = state.pos;
          if (state.eat(0x5C /* \ */) && state.eat(0x75 /* u */) && this.regexp_eatFixedHexDigits(state, 4)) {
            var trail = state.lastIntValue;
            if (trail >= 0xDC00 && trail <= 0xDFFF) {
              state.lastIntValue = (lead - 0xD800) * 0x400 + (trail - 0xDC00) + 0x10000;
              return true
            }
          }
          state.pos = leadSurrogateEnd;
          state.lastIntValue = lead;
        }
        return true
      }
      if (
        switchU &&
        state.eat(0x7B /* { */) &&
        this.regexp_eatHexDigits(state) &&
        state.eat(0x7D /* } */) &&
        isValidUnicode(state.lastIntValue)
      ) {
        return true
      }
      if (switchU) {
        state.raise("Invalid unicode escape");
      }
      state.pos = start;
    }

    return false
  };
  function isValidUnicode(ch) {
    return ch >= 0 && ch <= 0x10FFFF
  }

  // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-IdentityEscape
  pp$1.regexp_eatIdentityEscape = function(state) {
    if (state.switchU) {
      if (this.regexp_eatSyntaxCharacter(state)) {
        return true
      }
      if (state.eat(0x2F /* / */)) {
        state.lastIntValue = 0x2F; /* / */
        return true
      }
      return false
    }

    var ch = state.current();
    if (ch !== 0x63 /* c */ && (!state.switchN || ch !== 0x6B /* k */)) {
      state.lastIntValue = ch;
      state.advance();
      return true
    }

    return false
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-DecimalEscape
  pp$1.regexp_eatDecimalEscape = function(state) {
    state.lastIntValue = 0;
    var ch = state.current();
    if (ch >= 0x31 /* 1 */ && ch <= 0x39 /* 9 */) {
      do {
        state.lastIntValue = 10 * state.lastIntValue + (ch - 0x30 /* 0 */);
        state.advance();
      } while ((ch = state.current()) >= 0x30 /* 0 */ && ch <= 0x39 /* 9 */)
      return true
    }
    return false
  };

  // Return values used by character set parsing methods, needed to
  // forbid negation of sets that can match strings.
  var CharSetNone = 0; // Nothing parsed
  var CharSetOk = 1; // Construct parsed, cannot contain strings
  var CharSetString = 2; // Construct parsed, can contain strings

  // https://www.ecma-international.org/ecma-262/8.0/#prod-CharacterClassEscape
  pp$1.regexp_eatCharacterClassEscape = function(state) {
    var ch = state.current();

    if (isCharacterClassEscape(ch)) {
      state.lastIntValue = -1;
      state.advance();
      return CharSetOk
    }

    var negate = false;
    if (
      state.switchU &&
      this.options.ecmaVersion >= 9 &&
      ((negate = ch === 0x50 /* P */) || ch === 0x70 /* p */)
    ) {
      state.lastIntValue = -1;
      state.advance();
      var result;
      if (
        state.eat(0x7B /* { */) &&
        (result = this.regexp_eatUnicodePropertyValueExpression(state)) &&
        state.eat(0x7D /* } */)
      ) {
        if (negate && result === CharSetString) { state.raise("Invalid property name"); }
        return result
      }
      state.raise("Invalid property name");
    }

    return CharSetNone
  };

  function isCharacterClassEscape(ch) {
    return (
      ch === 0x64 /* d */ ||
      ch === 0x44 /* D */ ||
      ch === 0x73 /* s */ ||
      ch === 0x53 /* S */ ||
      ch === 0x77 /* w */ ||
      ch === 0x57 /* W */
    )
  }

  // UnicodePropertyValueExpression ::
  //   UnicodePropertyName `=` UnicodePropertyValue
  //   LoneUnicodePropertyNameOrValue
  pp$1.regexp_eatUnicodePropertyValueExpression = function(state) {
    var start = state.pos;

    // UnicodePropertyName `=` UnicodePropertyValue
    if (this.regexp_eatUnicodePropertyName(state) && state.eat(0x3D /* = */)) {
      var name = state.lastStringValue;
      if (this.regexp_eatUnicodePropertyValue(state)) {
        var value = state.lastStringValue;
        this.regexp_validateUnicodePropertyNameAndValue(state, name, value);
        return CharSetOk
      }
    }
    state.pos = start;

    // LoneUnicodePropertyNameOrValue
    if (this.regexp_eatLoneUnicodePropertyNameOrValue(state)) {
      var nameOrValue = state.lastStringValue;
      return this.regexp_validateUnicodePropertyNameOrValue(state, nameOrValue)
    }
    return CharSetNone
  };

  pp$1.regexp_validateUnicodePropertyNameAndValue = function(state, name, value) {
    if (!hasOwn(state.unicodeProperties.nonBinary, name))
      { state.raise("Invalid property name"); }
    if (!state.unicodeProperties.nonBinary[name].test(value))
      { state.raise("Invalid property value"); }
  };

  pp$1.regexp_validateUnicodePropertyNameOrValue = function(state, nameOrValue) {
    if (state.unicodeProperties.binary.test(nameOrValue)) { return CharSetOk }
    if (state.switchV && state.unicodeProperties.binaryOfStrings.test(nameOrValue)) { return CharSetString }
    state.raise("Invalid property name");
  };

  // UnicodePropertyName ::
  //   UnicodePropertyNameCharacters
  pp$1.regexp_eatUnicodePropertyName = function(state) {
    var ch = 0;
    state.lastStringValue = "";
    while (isUnicodePropertyNameCharacter(ch = state.current())) {
      state.lastStringValue += codePointToString(ch);
      state.advance();
    }
    return state.lastStringValue !== ""
  };

  function isUnicodePropertyNameCharacter(ch) {
    return isControlLetter(ch) || ch === 0x5F /* _ */
  }

  // UnicodePropertyValue ::
  //   UnicodePropertyValueCharacters
  pp$1.regexp_eatUnicodePropertyValue = function(state) {
    var ch = 0;
    state.lastStringValue = "";
    while (isUnicodePropertyValueCharacter(ch = state.current())) {
      state.lastStringValue += codePointToString(ch);
      state.advance();
    }
    return state.lastStringValue !== ""
  };
  function isUnicodePropertyValueCharacter(ch) {
    return isUnicodePropertyNameCharacter(ch) || isDecimalDigit(ch)
  }

  // LoneUnicodePropertyNameOrValue ::
  //   UnicodePropertyValueCharacters
  pp$1.regexp_eatLoneUnicodePropertyNameOrValue = function(state) {
    return this.regexp_eatUnicodePropertyValue(state)
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-CharacterClass
  pp$1.regexp_eatCharacterClass = function(state) {
    if (state.eat(0x5B /* [ */)) {
      var negate = state.eat(0x5E /* ^ */);
      var result = this.regexp_classContents(state);
      if (!state.eat(0x5D /* ] */))
        { state.raise("Unterminated character class"); }
      if (negate && result === CharSetString)
        { state.raise("Negated character class may contain strings"); }
      return true
    }
    return false
  };

  // https://tc39.es/ecma262/#prod-ClassContents
  // https://www.ecma-international.org/ecma-262/8.0/#prod-ClassRanges
  pp$1.regexp_classContents = function(state) {
    if (state.current() === 0x5D /* ] */) { return CharSetOk }
    if (state.switchV) { return this.regexp_classSetExpression(state) }
    this.regexp_nonEmptyClassRanges(state);
    return CharSetOk
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-NonemptyClassRanges
  // https://www.ecma-international.org/ecma-262/8.0/#prod-NonemptyClassRangesNoDash
  pp$1.regexp_nonEmptyClassRanges = function(state) {
    while (this.regexp_eatClassAtom(state)) {
      var left = state.lastIntValue;
      if (state.eat(0x2D /* - */) && this.regexp_eatClassAtom(state)) {
        var right = state.lastIntValue;
        if (state.switchU && (left === -1 || right === -1)) {
          state.raise("Invalid character class");
        }
        if (left !== -1 && right !== -1 && left > right) {
          state.raise("Range out of order in character class");
        }
      }
    }
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-ClassAtom
  // https://www.ecma-international.org/ecma-262/8.0/#prod-ClassAtomNoDash
  pp$1.regexp_eatClassAtom = function(state) {
    var start = state.pos;

    if (state.eat(0x5C /* \ */)) {
      if (this.regexp_eatClassEscape(state)) {
        return true
      }
      if (state.switchU) {
        // Make the same message as V8.
        var ch$1 = state.current();
        if (ch$1 === 0x63 /* c */ || isOctalDigit(ch$1)) {
          state.raise("Invalid class escape");
        }
        state.raise("Invalid escape");
      }
      state.pos = start;
    }

    var ch = state.current();
    if (ch !== 0x5D /* ] */) {
      state.lastIntValue = ch;
      state.advance();
      return true
    }

    return false
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-ClassEscape
  pp$1.regexp_eatClassEscape = function(state) {
    var start = state.pos;

    if (state.eat(0x62 /* b */)) {
      state.lastIntValue = 0x08; /* <BS> */
      return true
    }

    if (state.switchU && state.eat(0x2D /* - */)) {
      state.lastIntValue = 0x2D; /* - */
      return true
    }

    if (!state.switchU && state.eat(0x63 /* c */)) {
      if (this.regexp_eatClassControlLetter(state)) {
        return true
      }
      state.pos = start;
    }

    return (
      this.regexp_eatCharacterClassEscape(state) ||
      this.regexp_eatCharacterEscape(state)
    )
  };

  // https://tc39.es/ecma262/#prod-ClassSetExpression
  // https://tc39.es/ecma262/#prod-ClassUnion
  // https://tc39.es/ecma262/#prod-ClassIntersection
  // https://tc39.es/ecma262/#prod-ClassSubtraction
  pp$1.regexp_classSetExpression = function(state) {
    var result = CharSetOk, subResult;
    if (this.regexp_eatClassSetRange(state)) ; else if (subResult = this.regexp_eatClassSetOperand(state)) {
      if (subResult === CharSetString) { result = CharSetString; }
      // https://tc39.es/ecma262/#prod-ClassIntersection
      var start = state.pos;
      while (state.eatChars([0x26, 0x26] /* && */)) {
        if (
          state.current() !== 0x26 /* & */ &&
          (subResult = this.regexp_eatClassSetOperand(state))
        ) {
          if (subResult !== CharSetString) { result = CharSetOk; }
          continue
        }
        state.raise("Invalid character in character class");
      }
      if (start !== state.pos) { return result }
      // https://tc39.es/ecma262/#prod-ClassSubtraction
      while (state.eatChars([0x2D, 0x2D] /* -- */)) {
        if (this.regexp_eatClassSetOperand(state)) { continue }
        state.raise("Invalid character in character class");
      }
      if (start !== state.pos) { return result }
    } else {
      state.raise("Invalid character in character class");
    }
    // https://tc39.es/ecma262/#prod-ClassUnion
    for (;;) {
      if (this.regexp_eatClassSetRange(state)) { continue }
      subResult = this.regexp_eatClassSetOperand(state);
      if (!subResult) { return result }
      if (subResult === CharSetString) { result = CharSetString; }
    }
  };

  // https://tc39.es/ecma262/#prod-ClassSetRange
  pp$1.regexp_eatClassSetRange = function(state) {
    var start = state.pos;
    if (this.regexp_eatClassSetCharacter(state)) {
      var left = state.lastIntValue;
      if (state.eat(0x2D /* - */) && this.regexp_eatClassSetCharacter(state)) {
        var right = state.lastIntValue;
        if (left !== -1 && right !== -1 && left > right) {
          state.raise("Range out of order in character class");
        }
        return true
      }
      state.pos = start;
    }
    return false
  };

  // https://tc39.es/ecma262/#prod-ClassSetOperand
  pp$1.regexp_eatClassSetOperand = function(state) {
    if (this.regexp_eatClassSetCharacter(state)) { return CharSetOk }
    return this.regexp_eatClassStringDisjunction(state) || this.regexp_eatNestedClass(state)
  };

  // https://tc39.es/ecma262/#prod-NestedClass
  pp$1.regexp_eatNestedClass = function(state) {
    var start = state.pos;
    if (state.eat(0x5B /* [ */)) {
      var negate = state.eat(0x5E /* ^ */);
      var result = this.regexp_classContents(state);
      if (state.eat(0x5D /* ] */)) {
        if (negate && result === CharSetString) {
          state.raise("Negated character class may contain strings");
        }
        return result
      }
      state.pos = start;
    }
    if (state.eat(0x5C /* \ */)) {
      var result$1 = this.regexp_eatCharacterClassEscape(state);
      if (result$1) {
        return result$1
      }
      state.pos = start;
    }
    return null
  };

  // https://tc39.es/ecma262/#prod-ClassStringDisjunction
  pp$1.regexp_eatClassStringDisjunction = function(state) {
    var start = state.pos;
    if (state.eatChars([0x5C, 0x71] /* \q */)) {
      if (state.eat(0x7B /* { */)) {
        var result = this.regexp_classStringDisjunctionContents(state);
        if (state.eat(0x7D /* } */)) {
          return result
        }
      } else {
        // Make the same message as V8.
        state.raise("Invalid escape");
      }
      state.pos = start;
    }
    return null
  };

  // https://tc39.es/ecma262/#prod-ClassStringDisjunctionContents
  pp$1.regexp_classStringDisjunctionContents = function(state) {
    var result = this.regexp_classString(state);
    while (state.eat(0x7C /* | */)) {
      if (this.regexp_classString(state) === CharSetString) { result = CharSetString; }
    }
    return result
  };

  // https://tc39.es/ecma262/#prod-ClassString
  // https://tc39.es/ecma262/#prod-NonEmptyClassString
  pp$1.regexp_classString = function(state) {
    var count = 0;
    while (this.regexp_eatClassSetCharacter(state)) { count++; }
    return count === 1 ? CharSetOk : CharSetString
  };

  // https://tc39.es/ecma262/#prod-ClassSetCharacter
  pp$1.regexp_eatClassSetCharacter = function(state) {
    var start = state.pos;
    if (state.eat(0x5C /* \ */)) {
      if (
        this.regexp_eatCharacterEscape(state) ||
        this.regexp_eatClassSetReservedPunctuator(state)
      ) {
        return true
      }
      if (state.eat(0x62 /* b */)) {
        state.lastIntValue = 0x08; /* <BS> */
        return true
      }
      state.pos = start;
      return false
    }
    var ch = state.current();
    if (ch < 0 || ch === state.lookahead() && isClassSetReservedDoublePunctuatorCharacter(ch)) { return false }
    if (isClassSetSyntaxCharacter(ch)) { return false }
    state.advance();
    state.lastIntValue = ch;
    return true
  };

  // https://tc39.es/ecma262/#prod-ClassSetReservedDoublePunctuator
  function isClassSetReservedDoublePunctuatorCharacter(ch) {
    return (
      ch === 0x21 /* ! */ ||
      ch >= 0x23 /* # */ && ch <= 0x26 /* & */ ||
      ch >= 0x2A /* * */ && ch <= 0x2C /* , */ ||
      ch === 0x2E /* . */ ||
      ch >= 0x3A /* : */ && ch <= 0x40 /* @ */ ||
      ch === 0x5E /* ^ */ ||
      ch === 0x60 /* ` */ ||
      ch === 0x7E /* ~ */
    )
  }

  // https://tc39.es/ecma262/#prod-ClassSetSyntaxCharacter
  function isClassSetSyntaxCharacter(ch) {
    return (
      ch === 0x28 /* ( */ ||
      ch === 0x29 /* ) */ ||
      ch === 0x2D /* - */ ||
      ch === 0x2F /* / */ ||
      ch >= 0x5B /* [ */ && ch <= 0x5D /* ] */ ||
      ch >= 0x7B /* { */ && ch <= 0x7D /* } */
    )
  }

  // https://tc39.es/ecma262/#prod-ClassSetReservedPunctuator
  pp$1.regexp_eatClassSetReservedPunctuator = function(state) {
    var ch = state.current();
    if (isClassSetReservedPunctuator(ch)) {
      state.lastIntValue = ch;
      state.advance();
      return true
    }
    return false
  };

  // https://tc39.es/ecma262/#prod-ClassSetReservedPunctuator
  function isClassSetReservedPunctuator(ch) {
    return (
      ch === 0x21 /* ! */ ||
      ch === 0x23 /* # */ ||
      ch === 0x25 /* % */ ||
      ch === 0x26 /* & */ ||
      ch === 0x2C /* , */ ||
      ch === 0x2D /* - */ ||
      ch >= 0x3A /* : */ && ch <= 0x3E /* > */ ||
      ch === 0x40 /* @ */ ||
      ch === 0x60 /* ` */ ||
      ch === 0x7E /* ~ */
    )
  }

  // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-ClassControlLetter
  pp$1.regexp_eatClassControlLetter = function(state) {
    var ch = state.current();
    if (isDecimalDigit(ch) || ch === 0x5F /* _ */) {
      state.lastIntValue = ch % 0x20;
      state.advance();
      return true
    }
    return false
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-HexEscapeSequence
  pp$1.regexp_eatHexEscapeSequence = function(state) {
    var start = state.pos;
    if (state.eat(0x78 /* x */)) {
      if (this.regexp_eatFixedHexDigits(state, 2)) {
        return true
      }
      if (state.switchU) {
        state.raise("Invalid escape");
      }
      state.pos = start;
    }
    return false
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-DecimalDigits
  pp$1.regexp_eatDecimalDigits = function(state) {
    var start = state.pos;
    var ch = 0;
    state.lastIntValue = 0;
    while (isDecimalDigit(ch = state.current())) {
      state.lastIntValue = 10 * state.lastIntValue + (ch - 0x30 /* 0 */);
      state.advance();
    }
    return state.pos !== start
  };
  function isDecimalDigit(ch) {
    return ch >= 0x30 /* 0 */ && ch <= 0x39 /* 9 */
  }

  // https://www.ecma-international.org/ecma-262/8.0/#prod-HexDigits
  pp$1.regexp_eatHexDigits = function(state) {
    var start = state.pos;
    var ch = 0;
    state.lastIntValue = 0;
    while (isHexDigit(ch = state.current())) {
      state.lastIntValue = 16 * state.lastIntValue + hexToInt(ch);
      state.advance();
    }
    return state.pos !== start
  };
  function isHexDigit(ch) {
    return (
      (ch >= 0x30 /* 0 */ && ch <= 0x39 /* 9 */) ||
      (ch >= 0x41 /* A */ && ch <= 0x46 /* F */) ||
      (ch >= 0x61 /* a */ && ch <= 0x66 /* f */)
    )
  }
  function hexToInt(ch) {
    if (ch >= 0x41 /* A */ && ch <= 0x46 /* F */) {
      return 10 + (ch - 0x41 /* A */)
    }
    if (ch >= 0x61 /* a */ && ch <= 0x66 /* f */) {
      return 10 + (ch - 0x61 /* a */)
    }
    return ch - 0x30 /* 0 */
  }

  // https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-LegacyOctalEscapeSequence
  // Allows only 0-377(octal) i.e. 0-255(decimal).
  pp$1.regexp_eatLegacyOctalEscapeSequence = function(state) {
    if (this.regexp_eatOctalDigit(state)) {
      var n1 = state.lastIntValue;
      if (this.regexp_eatOctalDigit(state)) {
        var n2 = state.lastIntValue;
        if (n1 <= 3 && this.regexp_eatOctalDigit(state)) {
          state.lastIntValue = n1 * 64 + n2 * 8 + state.lastIntValue;
        } else {
          state.lastIntValue = n1 * 8 + n2;
        }
      } else {
        state.lastIntValue = n1;
      }
      return true
    }
    return false
  };

  // https://www.ecma-international.org/ecma-262/8.0/#prod-OctalDigit
  pp$1.regexp_eatOctalDigit = function(state) {
    var ch = state.current();
    if (isOctalDigit(ch)) {
      state.lastIntValue = ch - 0x30; /* 0 */
      state.advance();
      return true
    }
    state.lastIntValue = 0;
    return false
  };
  function isOctalDigit(ch) {
    return ch >= 0x30 /* 0 */ && ch <= 0x37 /* 7 */
  }

  // https://www.ecma-international.org/ecma-262/8.0/#prod-Hex4Digits
  // https://www.ecma-international.org/ecma-262/8.0/#prod-HexDigit
  // And HexDigit HexDigit in https://www.ecma-international.org/ecma-262/8.0/#prod-HexEscapeSequence
  pp$1.regexp_eatFixedHexDigits = function(state, length) {
    var start = state.pos;
    state.lastIntValue = 0;
    for (var i = 0; i < length; ++i) {
      var ch = state.current();
      if (!isHexDigit(ch)) {
        state.pos = start;
        return false
      }
      state.lastIntValue = 16 * state.lastIntValue + hexToInt(ch);
      state.advance();
    }
    return true
  };

  // Object type used to represent tokens. Note that normally, tokens
  // simply exist as properties on the parser object. This is only
  // used for the onToken callback and the external tokenizer.

  var Token = function Token(p) {
    this.type = p.type;
    this.value = p.value;
    this.start = p.start;
    this.end = p.end;
    if (p.options.locations)
      { this.loc = new SourceLocation(p, p.startLoc, p.endLoc); }
    if (p.options.ranges)
      { this.range = [p.start, p.end]; }
  };

  // ## Tokenizer

  var pp = Parser.prototype;

  // Move to the next token

  pp.next = function(ignoreEscapeSequenceInKeyword) {
    if (!ignoreEscapeSequenceInKeyword && this.type.keyword && this.containsEsc)
      { this.raiseRecoverable(this.start, "Escape sequence in keyword " + this.type.keyword); }
    if (this.options.onToken)
      { this.options.onToken(new Token(this)); }

    this.lastTokEnd = this.end;
    this.lastTokStart = this.start;
    this.lastTokEndLoc = this.endLoc;
    this.lastTokStartLoc = this.startLoc;
    this.nextToken();
  };

  pp.getToken = function() {
    this.next();
    return new Token(this)
  };

  // If we're in an ES6 environment, make parsers iterable
  if (typeof Symbol !== "undefined")
    { pp[Symbol.iterator] = function() {
      var this$1$1 = this;

      return {
        next: function () {
          var token = this$1$1.getToken();
          return {
            done: token.type === types$1.eof,
            value: token
          }
        }
      }
    }; }

  // Toggle strict mode. Re-reads the next number or string to please
  // pedantic tests (`"use strict"; 010;` should fail).

  // Read a single token, updating the parser object's token-related
  // properties.

  pp.nextToken = function() {
    var curContext = this.curContext();
    if (!curContext || !curContext.preserveSpace) { this.skipSpace(); }

    this.start = this.pos;
    if (this.options.locations) { this.startLoc = this.curPosition(); }
    if (this.pos >= this.input.length) { return this.finishToken(types$1.eof) }

    if (curContext.override) { return curContext.override(this) }
    else { this.readToken(this.fullCharCodeAtPos()); }
  };

  pp.readToken = function(code) {
    // Identifier or keyword. '\uXXXX' sequences are allowed in
    // identifiers, so '\' also dispatches to that.
    if (isIdentifierStart(code, this.options.ecmaVersion >= 6) || code === 92 /* '\' */)
      { return this.readWord() }

    return this.getTokenFromCode(code)
  };

  pp.fullCharCodeAtPos = function() {
    var code = this.input.charCodeAt(this.pos);
    if (code <= 0xd7ff || code >= 0xdc00) { return code }
    var next = this.input.charCodeAt(this.pos + 1);
    return next <= 0xdbff || next >= 0xe000 ? code : (code << 10) + next - 0x35fdc00
  };

  pp.skipBlockComment = function() {
    var startLoc = this.options.onComment && this.curPosition();
    var start = this.pos, end = this.input.indexOf("*/", this.pos += 2);
    if (end === -1) { this.raise(this.pos - 2, "Unterminated comment"); }
    this.pos = end + 2;
    if (this.options.locations) {
      for (var nextBreak = (void 0), pos = start; (nextBreak = nextLineBreak(this.input, pos, this.pos)) > -1;) {
        ++this.curLine;
        pos = this.lineStart = nextBreak;
      }
    }
    if (this.options.onComment)
      { this.options.onComment(true, this.input.slice(start + 2, end), start, this.pos,
                             startLoc, this.curPosition()); }
  };

  pp.skipLineComment = function(startSkip) {
    var start = this.pos;
    var startLoc = this.options.onComment && this.curPosition();
    var ch = this.input.charCodeAt(this.pos += startSkip);
    while (this.pos < this.input.length && !isNewLine(ch)) {
      ch = this.input.charCodeAt(++this.pos);
    }
    if (this.options.onComment)
      { this.options.onComment(false, this.input.slice(start + startSkip, this.pos), start, this.pos,
                             startLoc, this.curPosition()); }
  };

  // Called at the start of the parse and after every token. Skips
  // whitespace and comments, and.

  pp.skipSpace = function() {
    loop: while (this.pos < this.input.length) {
      var ch = this.input.charCodeAt(this.pos);
      switch (ch) {
      case 32: case 160: // ' '
        ++this.pos;
        break
      case 13:
        if (this.input.charCodeAt(this.pos + 1) === 10) {
          ++this.pos;
        }
      case 10: case 8232: case 8233:
        ++this.pos;
        if (this.options.locations) {
          ++this.curLine;
          this.lineStart = this.pos;
        }
        break
      case 47: // '/'
        switch (this.input.charCodeAt(this.pos + 1)) {
        case 42: // '*'
          this.skipBlockComment();
          break
        case 47:
          this.skipLineComment(2);
          break
        default:
          break loop
        }
        break
      default:
        if (ch > 8 && ch < 14 || ch >= 5760 && nonASCIIwhitespace.test(String.fromCharCode(ch))) {
          ++this.pos;
        } else {
          break loop
        }
      }
    }
  };

  // Called at the end of every token. Sets `end`, `val`, and
  // maintains `context` and `exprAllowed`, and skips the space after
  // the token, so that the next one's `start` will point at the
  // right position.

  pp.finishToken = function(type, val) {
    this.end = this.pos;
    if (this.options.locations) { this.endLoc = this.curPosition(); }
    var prevType = this.type;
    this.type = type;
    this.value = val;

    this.updateContext(prevType);
  };

  // ### Token reading

  // This is the function that is called to fetch the next token. It
  // is somewhat obscure, because it works in character codes rather
  // than characters, and because operator parsing has been inlined
  // into it.
  //
  // All in the name of speed.
  //
  pp.readToken_dot = function() {
    var next = this.input.charCodeAt(this.pos + 1);
    if (next >= 48 && next <= 57) { return this.readNumber(true) }
    var next2 = this.input.charCodeAt(this.pos + 2);
    if (this.options.ecmaVersion >= 6 && next === 46 && next2 === 46) { // 46 = dot '.'
      this.pos += 3;
      return this.finishToken(types$1.ellipsis)
    } else {
      ++this.pos;
      return this.finishToken(types$1.dot)
    }
  };

  pp.readToken_slash = function() { // '/'
    var next = this.input.charCodeAt(this.pos + 1);
    if (this.exprAllowed) { ++this.pos; return this.readRegexp() }
    if (next === 61) { return this.finishOp(types$1.assign, 2) }
    return this.finishOp(types$1.slash, 1)
  };

  pp.readToken_mult_modulo_exp = function(code) { // '%*'
    var next = this.input.charCodeAt(this.pos + 1);
    var size = 1;
    var tokentype = code === 42 ? types$1.star : types$1.modulo;

    // exponentiation operator ** and **=
    if (this.options.ecmaVersion >= 7 && code === 42 && next === 42) {
      ++size;
      tokentype = types$1.starstar;
      next = this.input.charCodeAt(this.pos + 2);
    }

    if (next === 61) { return this.finishOp(types$1.assign, size + 1) }
    return this.finishOp(tokentype, size)
  };

  pp.readToken_pipe_amp = function(code) { // '|&'
    var next = this.input.charCodeAt(this.pos + 1);
    if (next === code) {
      if (this.options.ecmaVersion >= 12) {
        var next2 = this.input.charCodeAt(this.pos + 2);
        if (next2 === 61) { return this.finishOp(types$1.assign, 3) }
      }
      return this.finishOp(code === 124 ? types$1.logicalOR : types$1.logicalAND, 2)
    }
    if (next === 61) { return this.finishOp(types$1.assign, 2) }
    return this.finishOp(code === 124 ? types$1.bitwiseOR : types$1.bitwiseAND, 1)
  };

  pp.readToken_caret = function() { // '^'
    var next = this.input.charCodeAt(this.pos + 1);
    if (next === 61) { return this.finishOp(types$1.assign, 2) }
    return this.finishOp(types$1.bitwiseXOR, 1)
  };

  pp.readToken_plus_min = function(code) { // '+-'
    var next = this.input.charCodeAt(this.pos + 1);
    if (next === code) {
      if (next === 45 && !this.inModule && this.input.charCodeAt(this.pos + 2) === 62 &&
          (this.lastTokEnd === 0 || lineBreak.test(this.input.slice(this.lastTokEnd, this.pos)))) {
        // A `-->` line comment
        this.skipLineComment(3);
        this.skipSpace();
        return this.nextToken()
      }
      return this.finishOp(types$1.incDec, 2)
    }
    if (next === 61) { return this.finishOp(types$1.assign, 2) }
    return this.finishOp(types$1.plusMin, 1)
  };

  pp.readToken_lt_gt = function(code) { // '<>'
    var next = this.input.charCodeAt(this.pos + 1);
    var size = 1;
    if (next === code) {
      size = code === 62 && this.input.charCodeAt(this.pos + 2) === 62 ? 3 : 2;
      if (this.input.charCodeAt(this.pos + size) === 61) { return this.finishOp(types$1.assign, size + 1) }
      return this.finishOp(types$1.bitShift, size)
    }
    if (next === 33 && code === 60 && !this.inModule && this.input.charCodeAt(this.pos + 2) === 45 &&
        this.input.charCodeAt(this.pos + 3) === 45) {
      // `<!--`, an XML-style comment that should be interpreted as a line comment
      this.skipLineComment(4);
      this.skipSpace();
      return this.nextToken()
    }
    if (next === 61) { size = 2; }
    return this.finishOp(types$1.relational, size)
  };

  pp.readToken_eq_excl = function(code) { // '=!'
    var next = this.input.charCodeAt(this.pos + 1);
    if (next === 61) { return this.finishOp(types$1.equality, this.input.charCodeAt(this.pos + 2) === 61 ? 3 : 2) }
    if (code === 61 && next === 62 && this.options.ecmaVersion >= 6) { // '=>'
      this.pos += 2;
      return this.finishToken(types$1.arrow)
    }
    return this.finishOp(code === 61 ? types$1.eq : types$1.prefix, 1)
  };

  pp.readToken_question = function() { // '?'
    var ecmaVersion = this.options.ecmaVersion;
    if (ecmaVersion >= 11) {
      var next = this.input.charCodeAt(this.pos + 1);
      if (next === 46) {
        var next2 = this.input.charCodeAt(this.pos + 2);
        if (next2 < 48 || next2 > 57) { return this.finishOp(types$1.questionDot, 2) }
      }
      if (next === 63) {
        if (ecmaVersion >= 12) {
          var next2$1 = this.input.charCodeAt(this.pos + 2);
          if (next2$1 === 61) { return this.finishOp(types$1.assign, 3) }
        }
        return this.finishOp(types$1.coalesce, 2)
      }
    }
    return this.finishOp(types$1.question, 1)
  };

  pp.readToken_numberSign = function() { // '#'
    var ecmaVersion = this.options.ecmaVersion;
    var code = 35; // '#'
    if (ecmaVersion >= 13) {
      ++this.pos;
      code = this.fullCharCodeAtPos();
      if (isIdentifierStart(code, true) || code === 92 /* '\' */) {
        return this.finishToken(types$1.privateId, this.readWord1())
      }
    }

    this.raise(this.pos, "Unexpected character '" + codePointToString(code) + "'");
  };

  pp.getTokenFromCode = function(code) {
    switch (code) {
    // The interpretation of a dot depends on whether it is followed
    // by a digit or another two dots.
    case 46: // '.'
      return this.readToken_dot()

    // Punctuation tokens.
    case 40: ++this.pos; return this.finishToken(types$1.parenL)
    case 41: ++this.pos; return this.finishToken(types$1.parenR)
    case 59: ++this.pos; return this.finishToken(types$1.semi)
    case 44: ++this.pos; return this.finishToken(types$1.comma)
    case 91: ++this.pos; return this.finishToken(types$1.bracketL)
    case 93: ++this.pos; return this.finishToken(types$1.bracketR)
    case 123: ++this.pos; return this.finishToken(types$1.braceL)
    case 125: ++this.pos; return this.finishToken(types$1.braceR)
    case 58: ++this.pos; return this.finishToken(types$1.colon)

    case 96: // '`'
      if (this.options.ecmaVersion < 6) { break }
      ++this.pos;
      return this.finishToken(types$1.backQuote)

    case 48: // '0'
      var next = this.input.charCodeAt(this.pos + 1);
      if (next === 120 || next === 88) { return this.readRadixNumber(16) } // '0x', '0X' - hex number
      if (this.options.ecmaVersion >= 6) {
        if (next === 111 || next === 79) { return this.readRadixNumber(8) } // '0o', '0O' - octal number
        if (next === 98 || next === 66) { return this.readRadixNumber(2) } // '0b', '0B' - binary number
      }

    // Anything else beginning with a digit is an integer, octal
    // number, or float.
    case 49: case 50: case 51: case 52: case 53: case 54: case 55: case 56: case 57: // 1-9
      return this.readNumber(false)

    // Quotes produce strings.
    case 34: case 39: // '"', "'"
      return this.readString(code)

    // Operators are parsed inline in tiny state machines. '=' (61) is
    // often referred to. `finishOp` simply skips the amount of
    // characters it is given as second argument, and returns a token
    // of the type given by its first argument.
    case 47: // '/'
      return this.readToken_slash()

    case 37: case 42: // '%*'
      return this.readToken_mult_modulo_exp(code)

    case 124: case 38: // '|&'
      return this.readToken_pipe_amp(code)

    case 94: // '^'
      return this.readToken_caret()

    case 43: case 45: // '+-'
      return this.readToken_plus_min(code)

    case 60: case 62: // '<>'
      return this.readToken_lt_gt(code)

    case 61: case 33: // '=!'
      return this.readToken_eq_excl(code)

    case 63: // '?'
      return this.readToken_question()

    case 126: // '~'
      return this.finishOp(types$1.prefix, 1)

    case 35: // '#'
      return this.readToken_numberSign()
    }

    this.raise(this.pos, "Unexpected character '" + codePointToString(code) + "'");
  };

  pp.finishOp = function(type, size) {
    var str = this.input.slice(this.pos, this.pos + size);
    this.pos += size;
    return this.finishToken(type, str)
  };

  pp.readRegexp = function() {
    var escaped, inClass, start = this.pos;
    for (;;) {
      if (this.pos >= this.input.length) { this.raise(start, "Unterminated regular expression"); }
      var ch = this.input.charAt(this.pos);
      if (lineBreak.test(ch)) { this.raise(start, "Unterminated regular expression"); }
      if (!escaped) {
        if (ch === "[") { inClass = true; }
        else if (ch === "]" && inClass) { inClass = false; }
        else if (ch === "/" && !inClass) { break }
        escaped = ch === "\\";
      } else { escaped = false; }
      ++this.pos;
    }
    var pattern = this.input.slice(start, this.pos);
    ++this.pos;
    var flagsStart = this.pos;
    var flags = this.readWord1();
    if (this.containsEsc) { this.unexpected(flagsStart); }

    // Validate pattern
    var state = this.regexpState || (this.regexpState = new RegExpValidationState(this));
    state.reset(start, pattern, flags);
    this.validateRegExpFlags(state);
    this.validateRegExpPattern(state);

    // Create Literal#value property value.
    var value = null;
    try {
      value = new RegExp(pattern, flags);
    } catch (e) {
      // ESTree requires null if it failed to instantiate RegExp object.
      // https://github.com/estree/estree/blob/a27003adf4fd7bfad44de9cef372a2eacd527b1c/es5.md#regexpliteral
    }

    return this.finishToken(types$1.regexp, {pattern: pattern, flags: flags, value: value})
  };

  // Read an integer in the given radix. Return null if zero digits
  // were read, the integer value otherwise. When `len` is given, this
  // will return `null` unless the integer has exactly `len` digits.

  pp.readInt = function(radix, len, maybeLegacyOctalNumericLiteral) {
    // `len` is used for character escape sequences. In that case, disallow separators.
    var allowSeparators = this.options.ecmaVersion >= 12 && len === undefined;

    // `maybeLegacyOctalNumericLiteral` is true if it doesn't have prefix (0x,0o,0b)
    // and isn't fraction part nor exponent part. In that case, if the first digit
    // is zero then disallow separators.
    var isLegacyOctalNumericLiteral = maybeLegacyOctalNumericLiteral && this.input.charCodeAt(this.pos) === 48;

    var start = this.pos, total = 0, lastCode = 0;
    for (var i = 0, e = len == null ? Infinity : len; i < e; ++i, ++this.pos) {
      var code = this.input.charCodeAt(this.pos), val = (void 0);

      if (allowSeparators && code === 95) {
        if (isLegacyOctalNumericLiteral) { this.raiseRecoverable(this.pos, "Numeric separator is not allowed in legacy octal numeric literals"); }
        if (lastCode === 95) { this.raiseRecoverable(this.pos, "Numeric separator must be exactly one underscore"); }
        if (i === 0) { this.raiseRecoverable(this.pos, "Numeric separator is not allowed at the first of digits"); }
        lastCode = code;
        continue
      }

      if (code >= 97) { val = code - 97 + 10; } // a
      else if (code >= 65) { val = code - 65 + 10; } // A
      else if (code >= 48 && code <= 57) { val = code - 48; } // 0-9
      else { val = Infinity; }
      if (val >= radix) { break }
      lastCode = code;
      total = total * radix + val;
    }

    if (allowSeparators && lastCode === 95) { this.raiseRecoverable(this.pos - 1, "Numeric separator is not allowed at the last of digits"); }
    if (this.pos === start || len != null && this.pos - start !== len) { return null }

    return total
  };

  function stringToNumber(str, isLegacyOctalNumericLiteral) {
    if (isLegacyOctalNumericLiteral) {
      return parseInt(str, 8)
    }

    // `parseFloat(value)` stops parsing at the first numeric separator then returns a wrong value.
    return parseFloat(str.replace(/_/g, ""))
  }

  function stringToBigInt(str) {
    if (typeof BigInt !== "function") {
      return null
    }

    // `BigInt(value)` throws syntax error if the string contains numeric separators.
    return BigInt(str.replace(/_/g, ""))
  }

  pp.readRadixNumber = function(radix) {
    var start = this.pos;
    this.pos += 2; // 0x
    var val = this.readInt(radix);
    if (val == null) { this.raise(this.start + 2, "Expected number in radix " + radix); }
    if (this.options.ecmaVersion >= 11 && this.input.charCodeAt(this.pos) === 110) {
      val = stringToBigInt(this.input.slice(start, this.pos));
      ++this.pos;
    } else if (isIdentifierStart(this.fullCharCodeAtPos())) { this.raise(this.pos, "Identifier directly after number"); }
    return this.finishToken(types$1.num, val)
  };

  // Read an integer, octal integer, or floating-point number.

  pp.readNumber = function(startsWithDot) {
    var start = this.pos;
    if (!startsWithDot && this.readInt(10, undefined, true) === null) { this.raise(start, "Invalid number"); }
    var octal = this.pos - start >= 2 && this.input.charCodeAt(start) === 48;
    if (octal && this.strict) { this.raise(start, "Invalid number"); }
    var next = this.input.charCodeAt(this.pos);
    if (!octal && !startsWithDot && this.options.ecmaVersion >= 11 && next === 110) {
      var val$1 = stringToBigInt(this.input.slice(start, this.pos));
      ++this.pos;
      if (isIdentifierStart(this.fullCharCodeAtPos())) { this.raise(this.pos, "Identifier directly after number"); }
      return this.finishToken(types$1.num, val$1)
    }
    if (octal && /[89]/.test(this.input.slice(start, this.pos))) { octal = false; }
    if (next === 46 && !octal) { // '.'
      ++this.pos;
      this.readInt(10);
      next = this.input.charCodeAt(this.pos);
    }
    if ((next === 69 || next === 101) && !octal) { // 'eE'
      next = this.input.charCodeAt(++this.pos);
      if (next === 43 || next === 45) { ++this.pos; } // '+-'
      if (this.readInt(10) === null) { this.raise(start, "Invalid number"); }
    }
    if (isIdentifierStart(this.fullCharCodeAtPos())) { this.raise(this.pos, "Identifier directly after number"); }

    var val = stringToNumber(this.input.slice(start, this.pos), octal);
    return this.finishToken(types$1.num, val)
  };

  // Read a string value, interpreting backslash-escapes.

  pp.readCodePoint = function() {
    var ch = this.input.charCodeAt(this.pos), code;

    if (ch === 123) { // '{'
      if (this.options.ecmaVersion < 6) { this.unexpected(); }
      var codePos = ++this.pos;
      code = this.readHexChar(this.input.indexOf("}", this.pos) - this.pos);
      ++this.pos;
      if (code > 0x10FFFF) { this.invalidStringToken(codePos, "Code point out of bounds"); }
    } else {
      code = this.readHexChar(4);
    }
    return code
  };

  pp.readString = function(quote) {
    var out = "", chunkStart = ++this.pos;
    for (;;) {
      if (this.pos >= this.input.length) { this.raise(this.start, "Unterminated string constant"); }
      var ch = this.input.charCodeAt(this.pos);
      if (ch === quote) { break }
      if (ch === 92) { // '\'
        out += this.input.slice(chunkStart, this.pos);
        out += this.readEscapedChar(false);
        chunkStart = this.pos;
      } else if (ch === 0x2028 || ch === 0x2029) {
        if (this.options.ecmaVersion < 10) { this.raise(this.start, "Unterminated string constant"); }
        ++this.pos;
        if (this.options.locations) {
          this.curLine++;
          this.lineStart = this.pos;
        }
      } else {
        if (isNewLine(ch)) { this.raise(this.start, "Unterminated string constant"); }
        ++this.pos;
      }
    }
    out += this.input.slice(chunkStart, this.pos++);
    return this.finishToken(types$1.string, out)
  };

  // Reads template string tokens.

  var INVALID_TEMPLATE_ESCAPE_ERROR = {};

  pp.tryReadTemplateToken = function() {
    this.inTemplateElement = true;
    try {
      this.readTmplToken();
    } catch (err) {
      if (err === INVALID_TEMPLATE_ESCAPE_ERROR) {
        this.readInvalidTemplateToken();
      } else {
        throw err
      }
    }

    this.inTemplateElement = false;
  };

  pp.invalidStringToken = function(position, message) {
    if (this.inTemplateElement && this.options.ecmaVersion >= 9) {
      throw INVALID_TEMPLATE_ESCAPE_ERROR
    } else {
      this.raise(position, message);
    }
  };

  pp.readTmplToken = function() {
    var out = "", chunkStart = this.pos;
    for (;;) {
      if (this.pos >= this.input.length) { this.raise(this.start, "Unterminated template"); }
      var ch = this.input.charCodeAt(this.pos);
      if (ch === 96 || ch === 36 && this.input.charCodeAt(this.pos + 1) === 123) { // '`', '${'
        if (this.pos === this.start && (this.type === types$1.template || this.type === types$1.invalidTemplate)) {
          if (ch === 36) {
            this.pos += 2;
            return this.finishToken(types$1.dollarBraceL)
          } else {
            ++this.pos;
            return this.finishToken(types$1.backQuote)
          }
        }
        out += this.input.slice(chunkStart, this.pos);
        return this.finishToken(types$1.template, out)
      }
      if (ch === 92) { // '\'
        out += this.input.slice(chunkStart, this.pos);
        out += this.readEscapedChar(true);
        chunkStart = this.pos;
      } else if (isNewLine(ch)) {
        out += this.input.slice(chunkStart, this.pos);
        ++this.pos;
        switch (ch) {
        case 13:
          if (this.input.charCodeAt(this.pos) === 10) { ++this.pos; }
        case 10:
          out += "\n";
          break
        default:
          out += String.fromCharCode(ch);
          break
        }
        if (this.options.locations) {
          ++this.curLine;
          this.lineStart = this.pos;
        }
        chunkStart = this.pos;
      } else {
        ++this.pos;
      }
    }
  };

  // Reads a template token to search for the end, without validating any escape sequences
  pp.readInvalidTemplateToken = function() {
    for (; this.pos < this.input.length; this.pos++) {
      switch (this.input[this.pos]) {
      case "\\":
        ++this.pos;
        break

      case "$":
        if (this.input[this.pos + 1] !== "{") { break }
        // fall through
      case "`":
        return this.finishToken(types$1.invalidTemplate, this.input.slice(this.start, this.pos))

      case "\r":
        if (this.input[this.pos + 1] === "\n") { ++this.pos; }
        // fall through
      case "\n": case "\u2028": case "\u2029":
        ++this.curLine;
        this.lineStart = this.pos + 1;
        break
      }
    }
    this.raise(this.start, "Unterminated template");
  };

  // Used to read escaped characters

  pp.readEscapedChar = function(inTemplate) {
    var ch = this.input.charCodeAt(++this.pos);
    ++this.pos;
    switch (ch) {
    case 110: return "\n" // 'n' -> '\n'
    case 114: return "\r" // 'r' -> '\r'
    case 120: return String.fromCharCode(this.readHexChar(2)) // 'x'
    case 117: return codePointToString(this.readCodePoint()) // 'u'
    case 116: return "\t" // 't' -> '\t'
    case 98: return "\b" // 'b' -> '\b'
    case 118: return "\u000b" // 'v' -> '\u000b'
    case 102: return "\f" // 'f' -> '\f'
    case 13: if (this.input.charCodeAt(this.pos) === 10) { ++this.pos; } // '\r\n'
    case 10: // ' \n'
      if (this.options.locations) { this.lineStart = this.pos; ++this.curLine; }
      return ""
    case 56:
    case 57:
      if (this.strict) {
        this.invalidStringToken(
          this.pos - 1,
          "Invalid escape sequence"
        );
      }
      if (inTemplate) {
        var codePos = this.pos - 1;

        this.invalidStringToken(
          codePos,
          "Invalid escape sequence in template string"
        );
      }
    default:
      if (ch >= 48 && ch <= 55) {
        var octalStr = this.input.substr(this.pos - 1, 3).match(/^[0-7]+/)[0];
        var octal = parseInt(octalStr, 8);
        if (octal > 255) {
          octalStr = octalStr.slice(0, -1);
          octal = parseInt(octalStr, 8);
        }
        this.pos += octalStr.length - 1;
        ch = this.input.charCodeAt(this.pos);
        if ((octalStr !== "0" || ch === 56 || ch === 57) && (this.strict || inTemplate)) {
          this.invalidStringToken(
            this.pos - 1 - octalStr.length,
            inTemplate
              ? "Octal literal in template string"
              : "Octal literal in strict mode"
          );
        }
        return String.fromCharCode(octal)
      }
      if (isNewLine(ch)) {
        // Unicode new line characters after \ get removed from output in both
        // template literals and strings
        if (this.options.locations) { this.lineStart = this.pos; ++this.curLine; }
        return ""
      }
      return String.fromCharCode(ch)
    }
  };

  // Used to read character escape sequences ('\x', '\u', '\U').

  pp.readHexChar = function(len) {
    var codePos = this.pos;
    var n = this.readInt(16, len);
    if (n === null) { this.invalidStringToken(codePos, "Bad character escape sequence"); }
    return n
  };

  // Read an identifier, and return it as a string. Sets `this.containsEsc`
  // to whether the word contained a '\u' escape.
  //
  // Incrementally adds only escaped chars, adding other chunks as-is
  // as a micro-optimization.

  pp.readWord1 = function() {
    this.containsEsc = false;
    var word = "", first = true, chunkStart = this.pos;
    var astral = this.options.ecmaVersion >= 6;
    while (this.pos < this.input.length) {
      var ch = this.fullCharCodeAtPos();
      if (isIdentifierChar(ch, astral)) {
        this.pos += ch <= 0xffff ? 1 : 2;
      } else if (ch === 92) { // "\"
        this.containsEsc = true;
        word += this.input.slice(chunkStart, this.pos);
        var escStart = this.pos;
        if (this.input.charCodeAt(++this.pos) !== 117) // "u"
          { this.invalidStringToken(this.pos, "Expecting Unicode escape sequence \\uXXXX"); }
        ++this.pos;
        var esc = this.readCodePoint();
        if (!(first ? isIdentifierStart : isIdentifierChar)(esc, astral))
          { this.invalidStringToken(escStart, "Invalid Unicode escape"); }
        word += codePointToString(esc);
        chunkStart = this.pos;
      } else {
        break
      }
      first = false;
    }
    return word + this.input.slice(chunkStart, this.pos)
  };

  // Read an identifier or keyword token. Will check for reserved
  // words when necessary.

  pp.readWord = function() {
    var word = this.readWord1();
    var type = types$1.name;
    if (this.keywords.test(word)) {
      type = keywords[word];
    }
    return this.finishToken(type, word)
  };

  // Acorn is a tiny, fast JavaScript parser written in JavaScript.
  //
  // Acorn was written by Marijn Haverbeke, Ingvar Stepanyan, and
  // various contributors and released under an MIT license.
  //
  // Git repositories for Acorn are available at
  //
  //     http://marijnhaverbeke.nl/git/acorn
  //     https://github.com/acornjs/acorn.git
  //
  // Please use the [github bug tracker][ghbt] to report issues.
  //
  // [ghbt]: https://github.com/acornjs/acorn/issues


  var version$1 = "8.15.0";

  Parser.acorn = {
    Parser: Parser,
    version: version$1,
    defaultOptions: defaultOptions,
    Position: Position,
    SourceLocation: SourceLocation,
    getLineInfo: getLineInfo,
    Node: Node,
    TokenType: TokenType,
    tokTypes: types$1,
    keywordTypes: keywords,
    TokContext: TokContext,
    tokContexts: types,
    isIdentifierChar: isIdentifierChar,
    isIdentifierStart: isIdentifierStart,
    Token: Token,
    isNewLine: isNewLine,
    lineBreak: lineBreak,
    lineBreakG: lineBreakG,
    nonASCIIwhitespace: nonASCIIwhitespace
  };

  // The main exported interface (under `self.acorn` when in the
  // browser) is a `parse` function that takes a code string and returns
  // an abstract syntax tree as specified by the [ESTree spec][estree].
  //
  // [estree]: https://github.com/estree/estree

  function parse(input, options) {
    return Parser.parse(input, options)
  }

  // AST walker module for ESTree compatible trees

  // A simple walk is one where you simply specify callbacks to be
  // called on specific nodes. The last two arguments are optional. A
  // simple use would be
  //
  //     walk.simple(myTree, {
  //         Expression: function(node) { ... }
  //     });
  //
  // to do something with all expressions. All ESTree node types
  // can be used to identify node types, as well as Expression and
  // Statement, which denote categories of nodes.
  //
  // The base argument can be used to pass a custom (recursive)
  // walker, and state can be used to give this walked an initial
  // state.

  function simple(node, visitors, baseVisitor, state, override) {
    if (!baseVisitor) { baseVisitor = base
    ; }(function c(node, st, override) {
      var type = override || node.type;
      baseVisitor[type](node, st, c);
      if (visitors[type]) { visitors[type](node, st); }
    })(node, state, override);
  }

  // An ancestor walk keeps an array of ancestor nodes (including the
  // current node) and passes them to the callback as third parameter
  // (and also as state parameter when no other state is present).
  function ancestor(node, visitors, baseVisitor, state, override) {
    var ancestors = [];
    if (!baseVisitor) { baseVisitor = base
    ; }(function c(node, st, override) {
      var type = override || node.type;
      var isNew = node !== ancestors[ancestors.length - 1];
      if (isNew) { ancestors.push(node); }
      baseVisitor[type](node, st, c);
      if (visitors[type]) { visitors[type](node, st || ancestors, ancestors); }
      if (isNew) { ancestors.pop(); }
    })(node, state, override);
  }

  // A recursive walk is one where your functions override the default
  // walkers. They can modify and replace the state parameter that's
  // threaded through the walk, and can opt how and whether to walk
  // their child nodes (by calling their third argument on these
  // nodes).
  function recursive(node, state, funcs, baseVisitor, override) {
    var visitor = funcs ? make(funcs) : baseVisitor
    ;(function c(node, st, override) {
      visitor[override || node.type](node, st, c);
    })(node, state, override);
  }

  // Used to create a custom walker. Will fill in all missing node
  // type properties with the defaults.
  function make(funcs, baseVisitor) {
    var visitor = Object.create(base);
    for (var type in funcs) { visitor[type] = funcs[type]; }
    return visitor
  }

  function skipThrough(node, st, c) { c(node, st); }
  function ignore(_node, _st, _c) {}

  // Node walkers.

  var base = {};

  base.Program = base.BlockStatement = base.StaticBlock = function (node, st, c) {
    for (var i = 0, list = node.body; i < list.length; i += 1)
      {
      var stmt = list[i];

      c(stmt, st, "Statement");
    }
  };
  base.Statement = skipThrough;
  base.EmptyStatement = ignore;
  base.ExpressionStatement = base.ParenthesizedExpression = base.ChainExpression =
    function (node, st, c) { return c(node.expression, st, "Expression"); };
  base.IfStatement = function (node, st, c) {
    c(node.test, st, "Expression");
    c(node.consequent, st, "Statement");
    if (node.alternate) { c(node.alternate, st, "Statement"); }
  };
  base.LabeledStatement = function (node, st, c) { return c(node.body, st, "Statement"); };
  base.BreakStatement = base.ContinueStatement = ignore;
  base.WithStatement = function (node, st, c) {
    c(node.object, st, "Expression");
    c(node.body, st, "Statement");
  };
  base.SwitchStatement = function (node, st, c) {
    c(node.discriminant, st, "Expression");
    for (var i = 0, list = node.cases; i < list.length; i += 1) {
      var cs = list[i];

      c(cs, st);
    }
  };
  base.SwitchCase = function (node, st, c) {
    if (node.test) { c(node.test, st, "Expression"); }
    for (var i = 0, list = node.consequent; i < list.length; i += 1)
      {
      var cons = list[i];

      c(cons, st, "Statement");
    }
  };
  base.ReturnStatement = base.YieldExpression = base.AwaitExpression = function (node, st, c) {
    if (node.argument) { c(node.argument, st, "Expression"); }
  };
  base.ThrowStatement = base.SpreadElement =
    function (node, st, c) { return c(node.argument, st, "Expression"); };
  base.TryStatement = function (node, st, c) {
    c(node.block, st, "Statement");
    if (node.handler) { c(node.handler, st); }
    if (node.finalizer) { c(node.finalizer, st, "Statement"); }
  };
  base.CatchClause = function (node, st, c) {
    if (node.param) { c(node.param, st, "Pattern"); }
    c(node.body, st, "Statement");
  };
  base.WhileStatement = base.DoWhileStatement = function (node, st, c) {
    c(node.test, st, "Expression");
    c(node.body, st, "Statement");
  };
  base.ForStatement = function (node, st, c) {
    if (node.init) { c(node.init, st, "ForInit"); }
    if (node.test) { c(node.test, st, "Expression"); }
    if (node.update) { c(node.update, st, "Expression"); }
    c(node.body, st, "Statement");
  };
  base.ForInStatement = base.ForOfStatement = function (node, st, c) {
    c(node.left, st, "ForInit");
    c(node.right, st, "Expression");
    c(node.body, st, "Statement");
  };
  base.ForInit = function (node, st, c) {
    if (node.type === "VariableDeclaration") { c(node, st); }
    else { c(node, st, "Expression"); }
  };
  base.DebuggerStatement = ignore;

  base.FunctionDeclaration = function (node, st, c) { return c(node, st, "Function"); };
  base.VariableDeclaration = function (node, st, c) {
    for (var i = 0, list = node.declarations; i < list.length; i += 1)
      {
      var decl = list[i];

      c(decl, st);
    }
  };
  base.VariableDeclarator = function (node, st, c) {
    c(node.id, st, "Pattern");
    if (node.init) { c(node.init, st, "Expression"); }
  };

  base.Function = function (node, st, c) {
    if (node.id) { c(node.id, st, "Pattern"); }
    for (var i = 0, list = node.params; i < list.length; i += 1)
      {
      var param = list[i];

      c(param, st, "Pattern");
    }
    c(node.body, st, node.expression ? "Expression" : "Statement");
  };

  base.Pattern = function (node, st, c) {
    if (node.type === "Identifier")
      { c(node, st, "VariablePattern"); }
    else if (node.type === "MemberExpression")
      { c(node, st, "MemberPattern"); }
    else
      { c(node, st); }
  };
  base.VariablePattern = ignore;
  base.MemberPattern = skipThrough;
  base.RestElement = function (node, st, c) { return c(node.argument, st, "Pattern"); };
  base.ArrayPattern = function (node, st, c) {
    for (var i = 0, list = node.elements; i < list.length; i += 1) {
      var elt = list[i];

      if (elt) { c(elt, st, "Pattern"); }
    }
  };
  base.ObjectPattern = function (node, st, c) {
    for (var i = 0, list = node.properties; i < list.length; i += 1) {
      var prop = list[i];

      if (prop.type === "Property") {
        if (prop.computed) { c(prop.key, st, "Expression"); }
        c(prop.value, st, "Pattern");
      } else if (prop.type === "RestElement") {
        c(prop.argument, st, "Pattern");
      }
    }
  };

  base.Expression = skipThrough;
  base.ThisExpression = base.Super = base.MetaProperty = ignore;
  base.ArrayExpression = function (node, st, c) {
    for (var i = 0, list = node.elements; i < list.length; i += 1) {
      var elt = list[i];

      if (elt) { c(elt, st, "Expression"); }
    }
  };
  base.ObjectExpression = function (node, st, c) {
    for (var i = 0, list = node.properties; i < list.length; i += 1)
      {
      var prop = list[i];

      c(prop, st);
    }
  };
  base.FunctionExpression = base.ArrowFunctionExpression = base.FunctionDeclaration;
  base.SequenceExpression = function (node, st, c) {
    for (var i = 0, list = node.expressions; i < list.length; i += 1)
      {
      var expr = list[i];

      c(expr, st, "Expression");
    }
  };
  base.TemplateLiteral = function (node, st, c) {
    for (var i = 0, list = node.quasis; i < list.length; i += 1)
      {
      var quasi = list[i];

      c(quasi, st);
    }

    for (var i$1 = 0, list$1 = node.expressions; i$1 < list$1.length; i$1 += 1)
      {
      var expr = list$1[i$1];

      c(expr, st, "Expression");
    }
  };
  base.TemplateElement = ignore;
  base.UnaryExpression = base.UpdateExpression = function (node, st, c) {
    c(node.argument, st, "Expression");
  };
  base.BinaryExpression = base.LogicalExpression = function (node, st, c) {
    c(node.left, st, "Expression");
    c(node.right, st, "Expression");
  };
  base.AssignmentExpression = base.AssignmentPattern = function (node, st, c) {
    c(node.left, st, "Pattern");
    c(node.right, st, "Expression");
  };
  base.ConditionalExpression = function (node, st, c) {
    c(node.test, st, "Expression");
    c(node.consequent, st, "Expression");
    c(node.alternate, st, "Expression");
  };
  base.NewExpression = base.CallExpression = function (node, st, c) {
    c(node.callee, st, "Expression");
    if (node.arguments)
      { for (var i = 0, list = node.arguments; i < list.length; i += 1)
        {
          var arg = list[i];

          c(arg, st, "Expression");
        } }
  };
  base.MemberExpression = function (node, st, c) {
    c(node.object, st, "Expression");
    if (node.computed) { c(node.property, st, "Expression"); }
  };
  base.ExportNamedDeclaration = base.ExportDefaultDeclaration = function (node, st, c) {
    if (node.declaration)
      { c(node.declaration, st, node.type === "ExportNamedDeclaration" || node.declaration.id ? "Statement" : "Expression"); }
    if (node.source) { c(node.source, st, "Expression"); }
  };
  base.ExportAllDeclaration = function (node, st, c) {
    if (node.exported)
      { c(node.exported, st); }
    c(node.source, st, "Expression");
  };
  base.ImportDeclaration = function (node, st, c) {
    for (var i = 0, list = node.specifiers; i < list.length; i += 1)
      {
      var spec = list[i];

      c(spec, st);
    }
    c(node.source, st, "Expression");
  };
  base.ImportExpression = function (node, st, c) {
    c(node.source, st, "Expression");
  };
  base.ImportSpecifier = base.ImportDefaultSpecifier = base.ImportNamespaceSpecifier = base.Identifier = base.PrivateIdentifier = base.Literal = ignore;

  base.TaggedTemplateExpression = function (node, st, c) {
    c(node.tag, st, "Expression");
    c(node.quasi, st, "Expression");
  };
  base.ClassDeclaration = base.ClassExpression = function (node, st, c) { return c(node, st, "Class"); };
  base.Class = function (node, st, c) {
    if (node.id) { c(node.id, st, "Pattern"); }
    if (node.superClass) { c(node.superClass, st, "Expression"); }
    c(node.body, st);
  };
  base.ClassBody = function (node, st, c) {
    for (var i = 0, list = node.body; i < list.length; i += 1)
      {
      var elt = list[i];

      c(elt, st);
    }
  };
  base.MethodDefinition = base.PropertyDefinition = base.Property = function (node, st, c) {
    if (node.computed) { c(node.key, st, "Expression"); }
    if (node.value) { c(node.value, st, "Expression"); }
  };

  // List of functions to ignore as they either are meant to be re-defined or
  // generate false positive outputs.
  const ignoreFunction = [
    'setup',
    'draw',
    'preload',
    'deviceMoved',
    'deviceTurned',
    'deviceShaken',
    'doubleClicked',
    'mousePressed',
    'mouseReleased',
    'mouseMoved',
    'mouseDragged',
    'mouseClicked',
    'mouseWheel',
    'touchStarted',
    'touchMoved',
    'touchEnded',
    'keyPressed',
    'keyReleased',
    'keyTyped',
    'windowResized',
    // 'name',
    // 'parent',
    // 'toString',
    // 'print',
    // 'stop',
    // 'onended'
  ];

  const verifierUtils = {

    /**
     * Fetches the contents of a script element in the user's sketch.
     *
     * @private
     * @method fetchScript
     * @param {HTMLScriptElement} script
     * @returns {Promise<string>}
     */
    fetchScript: async function (script) {
      if (script.src) {
        try {
          const contents = await fetch(script.src).then(res => res.text());
          return contents;
        } catch (error) {
          // TODO: Handle CORS error here.
          console.error('Error fetching script:', error);
          return '';
        }
      } else {
        return script.textContent;
      }
    },

    /**
     * Extracts the user-defined variables and functions from the user code with
     * the help of Espree parser.
     *
     * @private
     * @method extractUserDefinedVariablesAndFuncs
     * @param {String} code - The code to extract variables and functions from.
     * @returns {Object} An object containing the user's defined variables and functions.
     * @returns {Array<{name: string, line: number}>} [userDefinitions.variables] Array of user-defined variable names and their line numbers.
     * @returns {Array<{name: string, line: number}>} [userDefinitions.functions] Array of user-defined function names and their line numbers.
     */
    extractUserDefinedVariablesAndFuncs: function (code) {
      const userDefinitions = {
        variables: [],
        functions: []
      };
      // The line numbers from the parser are consistently off by one, add
      // `lineOffset` here to correct them.
      const lineOffset = -1;

      try {
        const ast = parse(code, {
          ecmaVersion: 2021,
          sourceType: 'module',
          locations: true  // This helps us get the line number.
        });

        simple(ast, {
          VariableDeclarator(node) {
            if (node.id.type === 'Identifier') {
              const category = node.init && ['ArrowFunctionExpression', 'FunctionExpression'].includes(node.init.type)
                ? 'functions'
                : 'variables';
              userDefinitions[category].push({
                name: node.id.name,
                line: node.loc.start.line + lineOffset
              });
            }
          },
          FunctionDeclaration(node) {
            if (node.id && node.id.type === 'Identifier') {
              userDefinitions.functions.push({
                name: node.id.name,
                line: node.loc.start.line + lineOffset
              });
            }
          },
          // We consider class declarations to be a special form of variable
          // declaration.
          ClassDeclaration(node) {
            if (node.id && node.id.type === 'Identifier') {
              userDefinitions.variables.push({
                name: node.id.name,
                line: node.loc.start.line + lineOffset
              });
            }
          }
        });
      } catch (error) {
        // TODO: Replace this with a friendly error message.
        console.error('Error parsing code:', error);
      }

      return userDefinitions;
    },

    /**
     * Checks user-defined variables and functions for conflicts with p5.js
     * constants and global functions.
     *
     * This function performs two main checks:
     * 1. Verifies if any user definition conflicts with p5.js constants.
     * 2. Checks if any user definition conflicts with global functions from
     * p5.js renderer classes.
     *
     * If a conflict is found, it reports a friendly error message and halts
     * further checking.
     *
     * @private
     * @param {Object} userDefinitions - An object containing user-defined variables and functions.
     * @param {Array<{name: string, line: number}>} userDefinitions.variables - Array of user-defined variable names and their line numbers.
     * @param {Array<{name: string, line: number}>} userDefinitions.functions - Array of user-defined function names and their line numbers.
     * @returns {boolean} - Returns true if a conflict is found, false otherwise.
     */
    checkForConstsAndFuncs: function (userDefinitions, p5) {
      const allDefinitions = [
        ...userDefinitions.variables,
        ...userDefinitions.functions
      ];

      // Helper function that generates a friendly error message that contains
      // the type of redefinition (constant or function), the name of the
      // redefinition, the line number in user's code, and a link to its
      // reference on the p5.js website.
      function generateFriendlyError(errorType, name, line) {
        const url = `https://p5js.org/reference/p5/${name}`;
        const message = `${errorType} "${name}" on line ${line} is being redeclared and conflicts with a p5.js ${errorType.toLowerCase()}. p5.js reference: ${url}`;
        return message;
      }

      // Checks for constant redefinitions.
      for (let { name, line } of allDefinitions) {
        const libDefinition = constants[name];
        if (libDefinition !== undefined) {
          const message = generateFriendlyError('Constant', name, line);
          console.log(message);
          return true;
        }
      }

      // The new rules for attaching anything to global are (if true for both of
      // the following):
      //   - It is a member of p5.prototype
      //   - Its name does not start with `_`
      const globalFunctions = new Set(
        Object.getOwnPropertyNames(p5.prototype)
          .filter(key => !key.startsWith('_') && key !== 'constructor')
      );

      for (let { name, line } of allDefinitions) {
        if (!ignoreFunction.includes(name) && globalFunctions.has(name)) {
          const message = generateFriendlyError('Function', name, line);
          console.log(message);
          return true;
        }
      }

      return false;
    },

    /**
     * Extracts the user's code from the script fetched. Note that this method
     * assumes that the user's code is always the last script element in the
     * sketch.
     *
     * @private
     * @method getUserCode
     * @returns {Promise<string>} The user's code as a string.
     */
    getUserCode: async function () {
      // TODO: think of a more robust way to get the user's code. Refer to
      // https://github.com/processing/p5.js/pull/7293.
      const scripts = document.querySelectorAll('script');
      const userCodeScript = scripts[scripts.length - 1];
      const userCode = await verifierUtils.fetchScript(userCodeScript);

      return userCode;
    },

    /**
     * @private
     */
    runFES: async function (p5) {
      const userCode = await verifierUtils.getUserCode();
      const userDefinedVariablesAndFuncs = verifierUtils
        .extractUserDefinedVariablesAndFuncs(userCode);

      verifierUtils.checkForConstsAndFuncs(userDefinedVariablesAndFuncs, p5);
    }
  };

  function sketchVerifier(p5, _fn, lifecycles) {
    lifecycles.presetup = async function() {
      if (!p5.disableFriendlyErrors && !p5.disableSketchChecker) {
        verifierUtils.runFES(p5);
      }
    };
  }

  if (typeof p5 !== 'undefined') {
    sketchVerifier(p5);
  }

  /**
   * @for p5
   * @requires core
   */

  function fileErrors(p5, fn){
    // mapping used by `_friendlyFileLoadError`
    const fileLoadErrorCases = (num, filePath) => {
      const suggestion = translator('fes.fileLoadError.suggestion', {
        filePath,
        url: 'https://github.com/processing/p5.js/wiki/Local-server'
      });
      switch (num) {
        case 0:
          return {
            message: translator('fes.fileLoadError.image', {
              suggestion
            }),
            method: 'loadImage'
          };
        case 1:
          return {
            message: translator('fes.fileLoadError.xml', {
              suggestion
            }),
            method: 'loadXML'
          };
        case 2:
          return {
            message: translator('fes.fileLoadError.table', {
              suggestion
            }),
            method: 'loadTable'
          };
        case 3:
          return {
            message: translator('fes.fileLoadError.strings', {
              suggestion
            }),
            method: 'loadStrings'
          };
        case 4:
          return {
            message: translator('fes.fileLoadError.font', {
              suggestion
            }),
            method: 'loadFont'
          };
        case 5:
          return {
            message: translator('fes.fileLoadError.json', {
              suggestion
            }),
            method: 'loadJSON'
          };
        case 6:
          return {
            message: translator('fes.fileLoadError.bytes', {
              suggestion
            }),
            method: 'loadBytes'
          };
        case 7:
          return {
            message: translator('fes.fileLoadError.large'),
            method: 'loadX'
          };
        case 8:
          return {
            message: translator('fes.fileLoadError.gif'),
            method: 'loadImage'
          };
      }
    };

    /**
     * Called internally if there is an error during file loading.
     *
     * Generates and prints a friendly error message using key:
     * "fes.fileLoadError.[*]".
     *
     * @method _friendlyFileLoadError
     * @private
     * @param  {Number} errorType   Number of file load error type
     * @param  {String} filePath    Path to file caused the error
     */
    p5._friendlyFileLoadError = function(errorType, filePath) {
      const { message, method } = fileLoadErrorCases(errorType, filePath);
      p5._friendlyError(message, method, 3);
    };
  }

  if (typeof p5 !== 'undefined') {
    fileErrors(p5);
  }

  function friendlyErrors (p5) {
    p5.registerAddon(fesCore);
    p5.registerAddon(stacktrace);
    p5.registerAddon(validateParams);
    p5.registerAddon(sketchVerifier);
    p5.registerAddon(fileErrors);
  }

  /**
   * @module Data
   * @submodule LocalStorage
   * @requires core
   *
   * This module defines the p5 methods for working with local storage
   */

  function storage(p5, fn){
    /**
     * Stores a value in the web browser's local storage.
     *
     * Web browsers can save small amounts of data using the built-in
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage" target="_blank">localStorage object</a>.
     * Data stored in `localStorage` can be retrieved at any point, even after
     * refreshing a page or restarting the browser. Data are stored as key-value
     * pairs.
     *
     * `storeItem()` makes it easy to store values in `localStorage` and
     * <a href="#/p5/getItem">getItem()</a> makes it easy to retrieve them.
     *
     * The first parameter, `key`, is the name of the value to be stored as a
     * string.
     *
     * The second parameter, `value`, is the value to be stored. Values can have
     * any type.
     *
     * Note: Sensitive data such as passwords or personal information shouldn't be
     * stored in `localStorage`.
     *
     * @method storeItem
     * @for p5
     * @param {String} key name of the value.
     * @param {String|Number|Boolean|Object|Array} value value to be stored.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Store the player's name.
     *   storeItem('name', 'Feist');
     *
     *   // Store the player's score.
     *   storeItem('score', 1234);
     *
     *   describe('The text "Feist: 1234" written in black on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(14);
     *
     *   // Retrieve the name.
     *   let name = getItem('name');
     *
     *   // Retrieve the score.
     *   let score = getItem('score');
     *
     *   // Display the score.
     *   text(`${name}: ${score}`, 50, 50);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create an object.
     *   let p = { x: 50, y: 50 };
     *
     *   // Store the object.
     *   storeItem('position', p);
     *
     *   describe('A white circle on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Retrieve the object.
     *   let p = getItem('position');
     *
     *   // Draw the circle.
     *   circle(p.x, p.y, 30);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.Color object.
     *   let c = color('deeppink');
     *
     *   // Store the object.
     *   storeItem('color', c);
     *
     *   describe('A pink circle on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Retrieve the object.
     *   let c = getItem('color');
     *
     *   // Style the circle.
     *   fill(c);
     *
     *   // Draw the circle.
     *   circle(50, 50, 30);
     * }
     */
    fn.storeItem = function(key, value) {
      if (typeof key !== 'string') {
        console.log(
          `The argument that you passed to storeItem() - ${key} is not a string.`
        );
      }
      if (key.endsWith('p5TypeID')) {
        console.log(
          `The argument that you passed to storeItem() - ${key} must not end with 'p5TypeID'.`
        );
      }

      if (typeof value === 'undefined') {
        console.log('You cannot store undefined variables using storeItem().');
      }
      let type = typeof value;
      switch (type) {
        case 'number':
        case 'boolean':
          value = value.toString();
          break;
        case 'object':
          if (value instanceof p5.Color) {
            type = 'p5.Color';
            value = value.toString();
          } else if (value instanceof p5.Vector) {
            type = 'p5.Vector';
            const coord = value.values;
            value = coord;
          }
          value = JSON.stringify(value);
          break;
      }

      localStorage.setItem(key, value);
      const typeKey = `${key}p5TypeID`;
      localStorage.setItem(typeKey, type);
    };

    /**
     * Returns a value in the web browser's local storage.
     *
     * Web browsers can save small amounts of data using the built-in
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage" target="_blank">localStorage object</a>.
     * Data stored in `localStorage` can be retrieved at any point, even after
     * refreshing a page or restarting the browser. Data are stored as key-value
     * pairs.
     *
     * <a href="#/p5/storeItem">storeItem()</a> makes it easy to store values in
     * `localStorage` and `getItem()` makes it easy to retrieve them.
     *
     * The first parameter, `key`, is the name of the value to be stored as a
     * string.
     *
     * The second parameter, `value`, is the value to be retrieved a string. For
     * example, calling `getItem('size')` retrieves the value with the key `size`.
     *
     * Note: Sensitive data such as passwords or personal information shouldn't be
     * stored in `localStorage`.
     *
     * @method getItem
     * @for p5
     * @param {String} key name of the value.
     * @return {String|Number|Boolean|Object|Array} stored item.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Store the player's name.
     *   storeItem('name', 'Feist');
     *
     *   // Store the player's score.
     *   storeItem('score', 1234);
     *
     *   describe('The text "Feist: 1234" written in black on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(14);
     *
     *   // Retrieve the name.
     *   let name = getItem('name');
     *
     *   // Retrieve the score.
     *   let score = getItem('score');
     *
     *   // Display the score.
     *   text(`${name}: ${score}`, 50, 50);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create an object.
     *   let p = { x: 50, y: 50 };
     *
     *   // Store the object.
     *   storeItem('position', p);
     *
     *   describe('A white circle on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Retrieve the object.
     *   let p = getItem('position');
     *
     *   // Draw the circle.
     *   circle(p.x, p.y, 30);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a p5.Color object.
     *   let c = color('deeppink');
     *
     *   // Store the object.
     *   storeItem('color', c);
     *
     *   describe('A pink circle on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Retrieve the object.
     *   let c = getItem('color');
     *
     *   // Style the circle.
     *   fill(c);
     *
     *   // Draw the circle.
     *   circle(50, 50, 30);
     * }
     */
    fn.getItem = function(key) {
      let value = localStorage.getItem(key);
      const type = localStorage.getItem(`${key}p5TypeID`);
      if (typeof type === 'undefined') {
        console.log(
          `Unable to determine type of item stored under ${key}in local storage. Did you save the item with something other than setItem()?`
        );
      } else if (value !== null) {
        switch (type) {
          case 'number':
            value = parseFloat(value);
            break;
          case 'boolean':
            value = value === 'true';
            break;
          case 'object':
            value = JSON.parse(value);
            break;
          case 'p5.Color':
            value = this.color(JSON.parse(value));
            break;
          case 'p5.Vector':
            value = JSON.parse(value);
            value = this.createVector(...value);
            break;
        }
      }
      return value;
    };

    /**
     * Removes all items in the web browser's local storage.
     *
     * Web browsers can save small amounts of data using the built-in
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage" target="_blank">localStorage object</a>.
     * Data stored in `localStorage` can be retrieved at any point, even after
     * refreshing a page or restarting the browser. Data are stored as key-value
     * pairs. Calling `clearStorage()` removes all data from `localStorage`.
     *
     * Note: Sensitive data such as passwords or personal information shouldn't be
     * stored in `localStorage`.
     *
     * @method clearStorage
     * @for p5
     *
     * @example
     * // Double-click to clear localStorage.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Store the player's name.
     *   storeItem('name', 'Feist');
     *
     *   // Store the player's score.
     *   storeItem('score', 1234);
     *
     *   describe(
     *     'The text "Feist: 1234" written in black on a gray background. The text "null: null" appears when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(14);
     *
     *   // Retrieve the name.
     *   let name = getItem('name');
     *
     *   // Retrieve the score.
     *   let score = getItem('score');
     *
     *   // Display the score.
     *   text(`${name}: ${score}`, 50, 50);
     * }
     *
     * // Clear localStorage when the user double-clicks.
     * function doubleClicked() {
     *   clearStorage();
     * }
     */
    fn.clearStorage = function () {
      const keys = Object.keys(localStorage);
      keys.forEach(key => {
        if (key.endsWith('p5TypeID')) {
          this.removeItem(key.replace('p5TypeID', ''));
        }
      });
    };

    /**
     * Removes an item from the web browser's local storage.
     *
     * Web browsers can save small amounts of data using the built-in
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage" target="_blank">localStorage object</a>.
     * Data stored in `localStorage` can be retrieved at any point, even after
     * refreshing a page or restarting the browser. Data are stored as key-value
     * pairs.
     *
     * <a href="#/p5/storeItem">storeItem()</a> makes it easy to store values in
     * `localStorage` and `removeItem()` makes it easy to delete them.
     *
     * The parameter, `key`, is the name of the value to remove as a string. For
     * example, calling `removeItem('size')` removes the item with the key `size`.
     *
     * Note: Sensitive data such as passwords or personal information shouldn't be
     * stored in `localStorage`.
     *
     * @method removeItem
     * @param {String} key name of the value to remove.
     * @for p5
     *
     * @example
     * // Double-click to remove an item from localStorage.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Store the player's name.
     *   storeItem('name', 'Feist');
     *
     *   // Store the player's score.
     *   storeItem('score', 1234);
     *
     *   describe(
     *     'The text "Feist: 1234" written in black on a gray background. The text "Feist: null" appears when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(14);
     *
     *   // Retrieve the name.
     *   let name = getItem('name');
     *
     *   // Retrieve the score.
     *   let score = getItem('score');
     *
     *   // Display the score.
     *   text(`${name}: ${score}`, 50, 50);
     * }
     *
     * // Remove the word from localStorage when the user double-clicks.
     * function doubleClicked() {
     *   removeItem('score');
     * }
     */
    fn.removeItem = function(key) {
      if (typeof key !== 'string') {
        console.log(
          `The argument that you passed to removeItem() - ${key} is not a string.`
        );
      }
      localStorage.removeItem(key);
      localStorage.removeItem(`${key}p5TypeID`);
    };
  }

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

  function data(p5){
    p5.registerAddon(storage);
  }

  /**
   * The web is much more than just canvas and the DOM functionality makes it easy to interact
   * with other HTML5 objects, including text, hyperlink, image, input, video,
   * audio, and webcam.
   * There is a set of creation methods, DOM manipulation methods, and
   * an extended <a href="#/p5.Element">p5.Element</a> that supports a range of HTML elements. See the
   * <a href='https://github.com/processing/p5.js/wiki/Beyond-the-canvas'>
   * beyond the canvas tutorial</a> for a full overview of how this addon works.
   *
   * See <a href='https://github.com/processing/p5.js/wiki/Beyond-the-canvas'>tutorial: beyond the canvas</a>
   * for more info on how to use this library.</a>
   *
   * @module DOM
   * @submodule DOM
   * @for p5
   * @requires p5
   */


  function dom$1(p5, fn){
    /**
     * Searches the page for the first element that matches the given
     * <a href="https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/CSS_basics#different_types_of_selectors" target="_blank">CSS selector string</a>.
     *
     * The selector string can be an ID, class, tag name, or a combination.
     * `select()` returns a <a href="#/p5.Element">p5.Element</a> object if it
     * finds a match and `null` if not.
     *
     * The second parameter, `container`, is optional. It specifies a container to
     * search within. `container` can be CSS selector string, a
     * <a href="#/p5.Element">p5.Element</a> object, or an
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement" target="_blank">HTMLElement</a> object.
     *
     * @method select
     * @param  {String} selectors CSS selector string of element to search for.
     * @param  {String|p5.Element|HTMLElement} [container] CSS selector string, <a href="#/p5.Element">p5.Element</a>, or
     *                                             <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement" target="_blank">HTMLElement</a> to search within.
     * @return {p5.Element|null} <a href="#/p5.Element">p5.Element</a> containing the element.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *   background(200);
     *
     *   // Select the canvas by its tag.
     *   let cnv = select('canvas');
     *   cnv.style('border', '5px deeppink dashed');
     *
     *   describe('A gray square with a dashed pink border.');
     * }
     *
     * @example
     * function setup() {
     *   let cnv = createCanvas(100, 100);
     *
     *   // Add a class attribute to the canvas.
     *   cnv.class('pinkborder');
     *
     *   background(200);
     *
     *   // Select the canvas by its class.
     *   cnv = select('.pinkborder');
     *
     *   // Style its border.
     *   cnv.style('border', '5px deeppink dashed');
     *
     *   describe('A gray square with a dashed pink border.');
     * }
     *
     * @example
     * function setup() {
     *   let cnv = createCanvas(100, 100);
     *
     *   // Set the canvas' ID.
     *   cnv.id('mycanvas');
     *
     *   background(200);
     *
     *   // Select the canvas by its ID.
     *   cnv = select('#mycanvas');
     *
     *   // Style its border.
     *   cnv.style('border', '5px deeppink dashed');
     *
     *   describe('A gray square with a dashed pink border.');
     * }
     */
    fn.select = function (e, p) {
      // p5._validateParameters('select', arguments);
      const container = this._getContainer(p);
      const res = container.querySelector(e);
      if (res) {
        return this._wrapElement(res);
      } else {
        return null;
      }
    };

    /**
     * Searches the page for all elements that matches the given
     * <a href="https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/CSS_basics#different_types_of_selectors" target="_blank">CSS selector string</a>.
     *
     * The selector string can be an ID, class, tag name, or a combination.
     * `selectAll()` returns an array of <a href="#/p5.Element">p5.Element</a>
     * objects if it finds any matches and an empty array if none are found.
     *
     * The second parameter, `container`, is optional. It specifies a container to
     * search within. `container` can be CSS selector string, a
     * <a href="#/p5.Element">p5.Element</a> object, or an
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement" target="_blank">HTMLElement</a> object.
     *
     * @method selectAll
     * @param  {String} selectors CSS selector string of element to search for.
     * @param  {String|p5.Element|HTMLElement} [container] CSS selector string, <a href="#/p5.Element">p5.Element</a>, or
     *                                             <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement" target="_blank">HTMLElement</a> to search within.
     * @return {p5.Element[]} array of <a href="#/p5.Element">p5.Element</a>s containing any elements found.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create three buttons.
     *   createButton('1');
     *   createButton('2');
     *   createButton('3');
     *
     *   // Select the buttons by their tag.
     *   let buttons = selectAll('button');
     *
     *   // Position the buttons.
     *   for (let i = 0; i < 3; i += 1) {
     *     buttons[i].position(0, i * 30);
     *   }
     *
     *   describe('Three buttons stacked vertically. The buttons are labeled, "1", "2", and "3".');
     * }
     *
     * @example
     * function setup() {
     *   // Create three buttons and position them.
     *   let b1 = createButton('1');
     *   b1.position(0, 0);
     *   let b2 = createButton('2');
     *   b2.position(0, 30);
     *   let b3 = createButton('3');
     *   b3.position(0, 60);
     *
     *   // Add a class attribute to each button.
     *   b1.class('btn');
     *   b2.class('btn btn-pink');
     *   b3.class('btn');
     *
     *   // Select the buttons by their class.
     *   let buttons = selectAll('.btn');
     *   let pinkButtons = selectAll('.btn-pink');
     *
     *   // Style the selected buttons.
     *   buttons.forEach(setFont);
     *   pinkButtons.forEach(setColor);
     *
     *   describe('Three buttons stacked vertically. The buttons are labeled, "1", "2", and "3". Buttons "1" and "3" are gray. Button "2" is pink.');
     * }
     *
     * // Set a button's font to Comic Sans MS.
     * function setFont(btn) {
     *   btn.style('font-family', 'Comic Sans MS');
     * }
     *
     * // Set a button's background and font color.
     * function setColor(btn) {
     *   btn.style('background', 'deeppink');
     *   btn.style('color', 'white');
     * }
     */
    fn.selectAll = function (e, p) {
      // p5._validateParameters('selectAll', arguments);
      const arr = [];
      const container = this._getContainer(p);
      const res = container.querySelectorAll(e);
      if (res) {
        for (let j = 0; j < res.length; j++) {
          const obj = this._wrapElement(res[j]);
          arr.push(obj);
        }
      }
      return arr;
    };

    /**
     * Helper function for select and selectAll
     */
    fn._getContainer = function (p) {
      let container = document;
      if (typeof p === 'string') {
        container = document.querySelector(p) || document;
      } else if (p instanceof Element) {
        container = p.elt;
      } else if (p instanceof HTMLElement) {
        container = p;
      }
      return container;
    };

    /**
     * Helper function for getElement and getElements.
     */
    fn._wrapElement = function (elt) {
      const children = Array.prototype.slice.call(elt.children);
      if (elt.tagName === 'INPUT' && elt.type === 'checkbox') {
        let converted = new Element(elt, this);
        converted.checked = function (...args) {
          if (args.length === 0) {
            return this.elt.checked;
          } else if (args[0]) {
            this.elt.checked = true;
          } else {
            this.elt.checked = false;
          }
          return this;
        };
        return converted;
      } else if (elt.tagName === 'VIDEO' || elt.tagName === 'AUDIO') {
        return new MediaElement(elt, this);
      } else if (elt.tagName === 'SELECT') {
        return this.createSelect(new Element(elt, this));
      } else if (
        children.length > 0 &&
        children.every(function (c) {
          return c.tagName === 'INPUT' || c.tagName === 'LABEL';
        }) &&
        (elt.tagName === 'DIV' || elt.tagName === 'SPAN')
      ) {
        return this.createRadio(new Element(elt, this));
      } else {
        return new Element(elt, this);
      }
    };

    /**
     * Creates a new <a href="#/p5.Element">p5.Element</a> object.
     *
     * The first parameter, `tag`, is a string an HTML tag such as `'h5'`.
     *
     * The second parameter, `content`, is optional. It's a string that sets the
     * HTML content to insert into the new element. New elements have no content
     * by default.
     *
     * @method createElement
     * @param  {String} tag tag for the new element.
     * @param  {String} [content] HTML content to insert into the element.
     * @return {p5.Element} new <a href="#/p5.Element">p5.Element</a> object.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an h5 element with nothing in it.
     *   createElement('h5');
     *
     *   describe('A gray square.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an h5 element with the content "p5*js".
     *   let h5 = createElement('h5', 'p5*js');
     *
     *   // Set the element's style and position.
     *   h5.style('color', 'deeppink');
     *   h5.position(30, 15);
     *
     *   describe('The text "p5*js" written in pink in the middle of a gray square.');
     * }
     */
    fn.createElement = function (tag, content) {
      // p5._validateParameters('createElement', arguments);
      const elt = document.createElement(tag);
      if (typeof content !== 'undefined') {
        elt.innerHTML = content;
      }
      return addElement(elt, this);
    };

    /**
     * Removes all elements created by p5.js, including any event handlers.
     *
     * There are two exceptions:
     * canvas elements created by <a href="#/p5/createCanvas">createCanvas()</a>
     * and <a href="#/p5.Renderer">p5.Render</a> objects created by
     * <a href="#/p5/createGraphics">createGraphics()</a>.
     *
     * @method removeElements
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a paragraph element and place
     *   // it in the middle of the canvas.
     *   let p = createP('p5*js');
     *   p.position(25, 25);
     *
     *   describe('A gray square with the text "p5*js" written in its center. The text disappears when the mouse is pressed.');
     * }
     *
     * // Remove all elements when the mouse is pressed.
     * function mousePressed() {
     *   removeElements();
     * }
     *
     * @example
     * let slider;
     *
     * function setup() {
     *   createCanvas(200, 200);
     *
     *   // Create a paragraph element and place
     *   // it at the top of the canvas.
     *   let p = createP('p5*js');
     *   p.position(25, 25);
     *
     *   // Create a slider element and place it
     *   // beneath the canvas.
     *   slider = createSlider(0, 255, 200);
     *   slider.position(0, 100);
     *
     *   describe('A gray square with the text "p5*js" written in its center and a range slider beneath it. The square changes color when the slider is moved. The text and slider disappear when the square is double-clicked.');
     * }
     *
     * function draw() {
     *   // Use the slider value to change the background color.
     *   let g = slider.value();
     *   background(g);
     * }
     *
     * // Remove all elements when the mouse is double-clicked.
     * function doubleClicked() {
     *   removeElements();
     * }
     */
    fn.removeElements = function (e) {
      // p5._validateParameters('removeElements', arguments);
      // el.remove splices from this._elements, so don't mix iteration with it
      const isNotCanvasElement = el => !(el.elt instanceof HTMLCanvasElement);
      const removeableElements = this._elements.filter(isNotCanvasElement);
      removeableElements.map(el => el.remove());
    };

    /**
     * Helpers for create methods.
     */
    function addElement(elt, pInst, media) {
      const node = pInst._userNode ? pInst._userNode : document.body;
      node.appendChild(elt);
      const c = media
        ? new MediaElement(elt, pInst)
        : new Element(elt, pInst);
      pInst._elements.push(c);
      return c;
    }

    /**
     * Creates a `&lt;div&gt;&lt;/div&gt;` element.
     *
     * `&lt;div&gt;&lt;/div&gt;` elements are commonly used as containers for
     * other elements.
     *
     * The parameter `html` is optional. It accepts a string that sets the
     * inner HTML of the new `&lt;div&gt;&lt;/div&gt;`.
     *
     * @method createDiv
     * @param  {String} [html] inner HTML for the new `&lt;div&gt;&lt;/div&gt;` element.
     * @return {p5.Element} new <a href="#/p5.Element">p5.Element</a> object.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a div element and set its position.
     *   let div = createDiv('p5*js');
     *   div.position(25, 35);
     *
     *   describe('A gray square with the text "p5*js" written in its center.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an h3 element within the div.
     *   let div = createDiv('<h3>p5*js</h3>');
     *   div.position(20, 5);
     *
     *   describe('A gray square with the text "p5*js" written in its center.');
     * }
     */
    fn.createDiv = function (html = '') {
      let elt = document.createElement('div');
      elt.innerHTML = html;
      return addElement(elt, this);
    };

    /**
     * Creates a paragraph element.
     *
     * `&lt;p&gt;&lt;/p&gt;` elements are commonly used for paragraph-length text.
     *
     * The parameter `html` is optional. It accepts a string that sets the
     * inner HTML of the new `&lt;p&gt;&lt;/p&gt;`.
     *
     * @method createP
     * @param  {String} [html] inner HTML for the new `&lt;p&gt;&lt;/p&gt;` element.
     * @return {p5.Element} new <a href="#/p5.Element">p5.Element</a> object.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a paragraph element and set its position.
     *   let p = createP('Tell me a story.');
     *   p.position(5, 0);
     *
     *   describe('A gray square displaying the text "Tell me a story." written in black.');
     * }
     */
    fn.createP = function (html = '') {
      let elt = document.createElement('p');
      elt.innerHTML = html;
      return addElement(elt, this);
    };

    /**
     * Creates a `&lt;span&gt;&lt;/span&gt;` element.
     *
     * `&lt;span&gt;&lt;/span&gt;` elements are commonly used as containers
     * for inline elements. For example, a `&lt;span&gt;&lt;/span&gt;`
     * can hold part of a sentence that's a
     * <span style="color: deeppink;">different</span> style.
     *
     * The parameter `html` is optional. It accepts a string that sets the
     * inner HTML of the new `&lt;span&gt;&lt;/span&gt;`.
     *
     * @method createSpan
     * @param  {String} [html] inner HTML for the new `&lt;span&gt;&lt;/span&gt;` element.
     * @return {p5.Element} new <a href="#/p5.Element">p5.Element</a> object.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a span element and set its position.
     *   let span = createSpan('p5*js');
     *   span.position(25, 35);
     *
     *   describe('A gray square with the text "p5*js" written in its center.');
     * }
     *
     * @example
     * function setup() {
     *   background(200);
     *
     *   // Create a div element as a container.
     *   let div = createDiv();
     *
     *   // Place the div at the center.
     *   div.position(25, 35);
     *
     *   // Create a span element.
     *   let s1 = createSpan('p5');
     *
     *   // Create a second span element.
     *   let s2 = createSpan('*');
     *
     *   // Set the second span's font color.
     *   s2.style('color', 'deeppink');
     *
     *   // Create a third span element.
     *   let s3 = createSpan('js');
     *
     *   // Add all the spans to the container div.
     *   s1.parent(div);
     *   s2.parent(div);
     *   s3.parent(div);
     *
     *   describe('A gray square with the text "p5*js" written in black at its center. The asterisk is pink.');
     * }
     */
    fn.createSpan = function (html = '') {
      let elt = document.createElement('span');
      elt.innerHTML = html;
      return addElement(elt, this);
    };

    /**
     * Creates an `&lt;img&gt;` element that can appear outside of the canvas.
     *
     * The first parameter, `src`, is a string with the path to the image file.
     * `src` should be a relative path, as in `'assets/image.png'`, or a URL, as
     * in `'https://example.com/image.png'`.
     *
     * The second parameter, `alt`, is a string with the
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/alt#usage_notes" target="_blank">alternate text</a>
     * for the image. An empty string `''` can be used for images that aren't displayed.
     *
     * The third parameter, `crossOrigin`, is optional. It's a string that sets the
     * <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes" target="_blank">crossOrigin property</a>
     * of the image. Use `'anonymous'` or `'use-credentials'` to fetch the image
     * with cross-origin access.
     *
     * The fourth parameter, `callback`, is also optional. It sets a function to
     * call after the image loads. The new image is passed to the callback
     * function as a <a href="#/p5.Element">p5.Element</a> object.
     *
     * @method createImg
     * @param  {String} src relative path or URL for the image.
     * @param  {String} alt alternate text for the image.
     * @return {p5.Element} new <a href="#/p5.Element">p5.Element</a> object.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   let img = createImg(
     *     '/assets/cat.jpg',
     *     'image of a cat'
     *   );
     *   img.position(0, 10);
     *
     *   describe('A gray square with a magenta asterisk in its center.');
     * }
     */
    /**
     * @method createImg
     * @param  {String} src
     * @param  {String} alt
     * @param  {String} [crossOrigin] crossOrigin property to use when fetching the image.
     * @param  {Function} [successCallback] function to call once the image loads. The new image will be passed
     *                                      to the function as a <a href="#/p5.Element">p5.Element</a> object.
     * @return {p5.Element} new <a href="#/p5.Element">p5.Element</a> object.
     */
    fn.createImg = function () {
      // p5._validateParameters('createImg', arguments);
      const elt = document.createElement('img');
      const args = arguments;
      let self;
      if (args.length > 1 && typeof args[1] === 'string') {
        elt.alt = args[1];
      }
      if (args.length > 2 && typeof args[2] === 'string') {
        elt.crossOrigin = args[2];
      }
      elt.src = args[0];
      self = addElement(elt, this);
      elt.addEventListener('load', function () {
        self.width = elt.offsetWidth || elt.width;
        self.height = elt.offsetHeight || elt.height;
        const last = args[args.length - 1];
        if (typeof last === 'function') last(self);
      });
      return self;
    };

    /**
     * Creates an `&lt;a&gt;&lt;/a&gt;` element that links to another web page.
     *
     * The first parmeter, `href`, is a string that sets the URL of the linked
     * page.
     *
     * The second parameter, `html`, is a string that sets the inner HTML of the
     * link. It's common to use text, images, or buttons as links.
     *
     * The third parameter, `target`, is optional. It's a string that tells the
     * web browser where to open the link. By default, links open in the current
     * browser tab. Passing `'_blank'` will cause the link to open in a new
     * browser tab. MDN describes a few
     * <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#target" target="_blank">other options</a>.
     *
     * @method createA
     * @param  {String} href       URL of linked page.
     * @param  {String} html       inner HTML of link element to display.
     * @param  {String} [target]   target where the new link should open,
     *                             either `'_blank'`, `'_self'`, `'_parent'`, or `'_top'`.
     * @return {p5.Element} new <a href="#/p5.Element">p5.Element</a> object.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an anchor element that links to p5js.org.
     *   let a = createA('https://p5js.org/', 'p5*js');
     *   a.position(25, 35);
     *
     *   describe('The text "p5*js" written at the center of a gray square.');
     * }
     *
     * @example
     * function setup() {
     *   background(200);
     *
     *   // Create an anchor tag that links to p5js.org.
     *   // Open the link in a new tab.
     *   let a = createA('https://p5js.org/', 'p5*js', '_blank');
     *   a.position(25, 35);
     *
     *   describe('The text "p5*js" written at the center of a gray square.');
     * }
     */
    fn.createA = function (href, html, target) {
      // p5._validateParameters('createA', arguments);
      const elt = document.createElement('a');
      elt.href = href;
      elt.innerHTML = html;
      if (target) elt.target = target;
      return addElement(elt, this);
    };

    /* INPUT */
    /**
     * Creates a slider `&lt;input&gt;&lt;/input&gt;` element.
     *
     * Range sliders are useful for quickly selecting numbers from a given range.
     *
     * The first two parameters, `min` and `max`, are numbers that set the
     * slider's minimum and maximum.
     *
     * The third parameter, `value`, is optional. It's a number that sets the
     * slider's default value.
     *
     * The fourth parameter, `step`, is also optional. It's a number that sets the
     * spacing between each value in the slider's range. Setting `step` to 0
     * allows the slider to move smoothly from `min` to `max`.
     *
     * @method createSlider
     * @param  {Number} min minimum value of the slider.
     * @param  {Number} max maximum value of the slider.
     * @param  {Number} [value] default value of the slider.
     * @param  {Number} [step] size for each step in the slider's range.
     * @return {p5.Element} new <a href="#/p5.Element">p5.Element</a> object.
     *
     * @example
     * let slider;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a slider and place it at the top of the canvas.
     *   slider = createSlider(0, 255);
     *   slider.position(10, 10);
     *   slider.size(80);
     *
     *   describe('A dark gray square with a range slider at the top. The square changes color when the slider is moved.');
     * }
     *
     * function draw() {
     *   // Use the slider as a grayscale value.
     *   let g = slider.value();
     *   background(g);
     * }
     *
     * @example
     * let slider;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a slider and place it at the top of the canvas.
     *   // Set its default value to 0.
     *   slider = createSlider(0, 255, 0);
     *   slider.position(10, 10);
     *   slider.size(80);
     *
     *   describe('A black square with a range slider at the top. The square changes color when the slider is moved.');
     * }
     *
     * function draw() {
     *   // Use the slider as a grayscale value.
     *   let g = slider.value();
     *   background(g);
     * }
     *
     * @example
     * let slider;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a slider and place it at the top of the canvas.
     *   // Set its default value to 0.
     *   // Set its step size to 50.
     *   slider = createSlider(0, 255, 0, 50);
     *   slider.position(10, 10);
     *   slider.size(80);
     *
     *   describe('A black square with a range slider at the top. The square changes color when the slider is moved.');
     * }
     *
     * function draw() {
     *   // Use the slider as a grayscale value.
     *   let g = slider.value();
     *   background(g);
     * }
     *
     * @example
     * let slider;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a slider and place it at the top of the canvas.
     *   // Set its default value to 0.
     *   // Set its step size to 0 so that it moves smoothly.
     *   slider = createSlider(0, 255, 0, 0);
     *   slider.position(10, 10);
     *   slider.size(80);
     *
     *   describe('A black square with a range slider at the top. The square changes color when the slider is moved.');
     * }
     *
     * function draw() {
     *   // Use the slider as a grayscale value.
     *   let g = slider.value();
     *   background(g);
     * }
     */
    fn.createSlider = function (min, max, value, step) {
      // p5._validateParameters('createSlider', arguments);
      const elt = document.createElement('input');
      elt.type = 'range';
      elt.min = min;
      elt.max = max;
      if (step === 0) {
        elt.step = 0.000000000000000001; // smallest valid step
      } else if (step) {
        elt.step = step;
      }
      if (typeof value === 'number') elt.value = value;
      return addElement(elt, this);
    };

    /**
     * Creates a `&lt;button&gt;&lt;/button&gt;` element.
     *
     * The first parameter, `label`, is a string that sets the label displayed on
     * the button.
     *
     * The second parameter, `value`, is optional. It's a string that sets the
     * button's value. See
     * <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#value" target="_blank">MDN</a>
     * for more details.
     *
     * @method createButton
     * @param  {String} label label displayed on the button.
     * @param  {String} [value] value of the button.
     * @return {p5.Element} new <a href="#/p5.Element">p5.Element</a> object.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a button and place it beneath the canvas.
     *   let button = createButton('click me');
     *   button.position(0, 100);
     *
     *   // Call repaint() when the button is pressed.
     *   button.mousePressed(repaint);
     *
     *   describe('A gray square with a button that says "click me" beneath it. The square changes color when the button is clicked.');
     * }
     *
     * // Change the background color.
     * function repaint() {
     *   let g = random(255);
     *   background(g);
     * }
     *
     * @example
     * let button;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a button and set its value to 0.
     *   // Place the button beneath the canvas.
     *   button = createButton('click me', 'red');
     *   button.position(0, 100);
     *
     *   // Call randomColor() when the button is pressed.
     *   button.mousePressed(randomColor);
     *
     *   describe('A red square with a button that says "click me" beneath it. The square changes color when the button is clicked.');
     * }
     *
     * function draw() {
     *   // Use the button's value to set the background color.
     *   let c = button.value();
     *   background(c);
     * }
     *
     * // Set the button's value to a random color.
     * function randomColor() {
     *   let c = random(['red', 'green', 'blue', 'yellow']);
     *   button.value(c);
     * }
     */
    fn.createButton = function (label, value) {
      // p5._validateParameters('createButton', arguments);
      const elt = document.createElement('button');
      elt.innerHTML = label;
      if (value) elt.value = value;
      return addElement(elt, this);
    };

    /**
     * Creates a checkbox `&lt;input&gt;&lt;/input&gt;` element.
     *
     * Checkboxes extend the <a href="#/p5.Element">p5.Element</a> class with a
     * `checked()` method. Calling `myBox.checked()` returns `true` if it the box
     * is checked and `false` if not.
     *
     * The first parameter, `label`, is optional. It's a string that sets the label
     * to display next to the checkbox.
     *
     * The second parameter, `value`, is also optional. It's a boolean that sets the
     * checkbox's value.
     *
     * @method createCheckbox
     * @param  {String} [label] label displayed after the checkbox.
     * @param  {Boolean} [value] value of the checkbox. Checked is `true` and unchecked is `false`.
     * @return {p5.Element} new <a href="#/p5.Element">p5.Element</a> object.
     *
     * @example
     * let checkbox;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a checkbox and place it beneath the canvas.
     *   checkbox = createCheckbox();
     *   checkbox.position(0, 70);
     *
     *   describe('A black square with a checkbox beneath it. The square turns white when the box is checked.');
     * }
     *
     * function draw() {
     *   // Use the checkbox to set the background color.
     *   if (checkbox.checked()) {
     *     background(255);
     *   } else {
     *     background(0);
     *   }
     * }
     *
     * @example
     * let checkbox;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a checkbox and place it beneath the canvas.
     *   // Label the checkbox "white".
     *   checkbox = createCheckbox(' white');
     *   checkbox.position(0, 70);
     *
     *   describe('A black square with a checkbox labeled "white" beneath it. The square turns white when the box is checked.');
     * }
     *
     * function draw() {
     *   // Use the checkbox to set the background color.
     *   if (checkbox.checked()) {
     *     background(255);
     *   } else {
     *     background(0);
     *   }
     * }
     *
     * @example
     * let checkbox;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a checkbox and place it beneath the canvas.
     *   // Label the checkbox "white" and set its value to true.
     *   checkbox = createCheckbox(' white', true);
     *   checkbox.position(0, 70);
     *
     *   describe('A white square with a checkbox labeled "white" beneath it. The square turns black when the box is unchecked.');
     * }
     *
     * function draw() {
     *   // Use the checkbox to set the background color.
     *   if (checkbox.checked()) {
     *     background(255);
     *   } else {
     *     background(0);
     *   }
     * }
     */
    fn.createCheckbox = function (...args) {
      // p5._validateParameters('createCheckbox', args);

      // Create a container element
      const elt = document.createElement('div');

      // Create checkbox type input element
      const checkbox = document.createElement('input');
      checkbox.type = 'checkbox';

      // Create label element and wrap it around checkbox
      const label = document.createElement('label');
      label.appendChild(checkbox);

      // Append label element inside the container
      elt.appendChild(label);

      //checkbox must be wrapped in p5.Element before label so that label appears after
      const self = addElement(elt, this);

      self.checked = function (...args) {
        const cb = self.elt.firstElementChild.getElementsByTagName('input')[0];
        if (cb) {
          if (args.length === 0) {
            return cb.checked;
          } else if (args[0]) {
            cb.checked = true;
          } else {
            cb.checked = false;
          }
        }
        return self;
      };

      this.value = function (val) {
        self.value = val;
        return this;
      };

      // Set the span element innerHTML as the label value if passed
      if (args[0]) {
        self.value(args[0]);
        const span = document.createElement('span');
        span.innerHTML = args[0];
        label.appendChild(span);
      }

      // Set the checked value of checkbox if passed
      if (args[1]) {
        checkbox.checked = true;
      }

      return self;
    };

    /**
     * Creates a dropdown menu `&lt;select&gt;&lt;/select&gt;` element.
     *
     * The parameter is optional. If `true` is passed, as in
     * `let mySelect = createSelect(true)`, then the dropdown will support
     * multiple selections. If an existing `&lt;select&gt;&lt;/select&gt;` element
     * is passed, as in `let mySelect = createSelect(otherSelect)`, the existing
     * element will be wrapped in a new <a href="#/p5.Element">p5.Element</a>
     * object.
     *
     * Dropdowns extend the <a href="#/p5.Element">p5.Element</a> class with a few
     * helpful methods for managing options:
     * - `mySelect.option(name, [value])` adds an option to the menu. The first paremeter, `name`, is a string that sets the option's name and value. The second parameter, `value`, is optional. If provided, it sets the value that corresponds to the key `name`. If an option with `name` already exists, its value is changed to `value`.
     * - `mySelect.value()` returns the currently-selected option's value.
     * - `mySelect.selected()` returns the currently-selected option.
     * - `mySelect.selected(option)` selects the given option by default.
     * - `mySelect.disable()` marks the whole dropdown element as disabled.
     * - `mySelect.disable(option)` marks a given option as disabled.
     * - `mySelect.enable()` marks the whole dropdown element as enabled.
     * - `mySelect.enable(option)` marks a given option as enabled.
     *
     * @method createSelect
     * @param {Boolean} [multiple] support multiple selections.
     * @return {p5.Element} new <a href="#/p5.Element">p5.Element</a> object.
     *
     * @example
     * let mySelect;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a dropdown and place it beneath the canvas.
     *   mySelect = createSelect();
     *   mySelect.position(0, 100);
     *
     *   // Add color options.
     *   mySelect.option('red');
     *   mySelect.option('green');
     *   mySelect.option('blue');
     *   mySelect.option('yellow');
     *
     *   // Set the selected option to "red".
     *   mySelect.selected('red');
     *
     *   describe('A red square with a dropdown menu beneath it. The square changes color when a new color is selected.');
     * }
     *
     * function draw() {
     *   // Use the selected value to paint the background.
     *   let c = mySelect.selected();
     *   background(c);
     * }
     *
     * @example
     * let mySelect;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a dropdown and place it beneath the canvas.
     *   mySelect = createSelect();
     *   mySelect.position(0, 100);
     *
     *   // Add color options.
     *   mySelect.option('red');
     *   mySelect.option('green');
     *   mySelect.option('blue');
     *   mySelect.option('yellow');
     *
     *   // Set the selected option to "red".
     *   mySelect.selected('red');
     *
     *   // Disable the "yellow" option.
     *   mySelect.disable('yellow');
     *
     *   describe('A red square with a dropdown menu beneath it. The square changes color when a new color is selected.');
     * }
     *
     * function draw() {
     *   // Use the selected value to paint the background.
     *   let c = mySelect.selected();
     *   background(c);
     * }
     *
     * @example
     * let mySelect;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a dropdown and place it beneath the canvas.
     *   mySelect = createSelect();
     *   mySelect.position(0, 100);
     *
     *   // Add color options with names and values.
     *   mySelect.option('one', 'red');
     *   mySelect.option('two', 'green');
     *   mySelect.option('three', 'blue');
     *   mySelect.option('four', 'yellow');
     *
     *   // Set the selected option to "one".
     *   mySelect.selected('one');
     *
     *   describe('A red square with a dropdown menu beneath it. The square changes color when a new color is selected.');
     * }
     *
     * function draw() {
     *   // Use the selected value to paint the background.
     *   let c = mySelect.selected();
     *   background(c);
     * }
     *
     * @example
     * // Hold CTRL to select multiple options on Windows and Linux.
     * // Hold CMD to select multiple options on macOS.
     * let mySelect;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a dropdown and allow multiple selections.
     *   // Place it beneath the canvas.
     *   mySelect = createSelect(true);
     *   mySelect.position(0, 100);
     *
     *   // Add color options.
     *   mySelect.option('red');
     *   mySelect.option('green');
     *   mySelect.option('blue');
     *   mySelect.option('yellow');
     *
     *   describe('A gray square with a dropdown menu beneath it. Colorful circles appear when their color is selected.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Use the selected value(s) to draw circles.
     *   let colors = mySelect.selected();
     *   for (let i = 0; i < colors.length; i += 1) {
     *     // Calculate the x-coordinate.
     *     let x = 10 + i * 20;
     *
     *     // Access the color.
     *     let c = colors[i];
     *
     *     // Draw the circle.
     *     fill(c);
     *     circle(x, 50, 20);
     *   }
     * }
     */
    /**
     * @method createSelect
     * @param {Object} existing select element to wrap, either as a <a href="#/p5.Element">p5.Element</a> or
     *                          a <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement" target="_blank">HTMLSelectElement</a>.
     * @return {p5.Element}
     */
    fn.createSelect = function (...args) {
      // p5._validateParameters('createSelect', args);
      let self;
      let arg = args[0];
      if (arg instanceof Element && arg.elt instanceof HTMLSelectElement) {
        // If given argument is p5.Element of select type
        self = arg;
        this.elt = arg.elt;
      } else if (arg instanceof HTMLSelectElement) {
        self = addElement(arg, this);
        this.elt = arg;
      } else {
        const elt = document.createElement('select');
        if (arg && typeof arg === 'boolean') {
          elt.setAttribute('multiple', 'true');
        }
        self = addElement(elt, this);
        this.elt = elt;
      }
      self.option = function (name, value) {
        let index;

        // if no name is passed, return
        if (name === undefined) {
          return;
        }
        //see if there is already an option with this name
        for (let i = 0; i < this.elt.length; i += 1) {
          if (this.elt[i].textContent === name) {
            index = i;
            break;
          }
        }
        //if there is an option with this name we will modify it
        if (index !== undefined) {
          //if the user passed in false then delete that option
          if (value === false) {
            this.elt.remove(index);
          } else {
            // Update the option at index with the value
            this.elt[index].value = value;
          }
        } else {
          //if it doesn't exist create it
          const opt = document.createElement('option');
          opt.textContent = name;
          opt.value = value === undefined ? name : value;
          this.elt.appendChild(opt);
          this._pInst._elements.push(opt);
        }
      };

      self.selected = function (value) {
        // Update selected status of option
        if (value !== undefined) {
          for (let i = 0; i < this.elt.length; i += 1) {
            if (this.elt[i].value.toString() === value.toString()) {
              this.elt.selectedIndex = i;
            }
          }
          return this;
        } else {
          if (this.elt.getAttribute('multiple')) {
            let arr = [];
            for (const selectedOption of this.elt.selectedOptions) {
              arr.push(selectedOption.value);
            }
            return arr;
          } else {
            return this.elt.value;
          }
        }
      };

      self.disable = function (value) {
        if (typeof value === 'string') {
          for (let i = 0; i < this.elt.length; i++) {
            if (this.elt[i].value.toString() === value) {
              this.elt[i].disabled = true;
              this.elt[i].selected = false;
            }
          }
        } else {
          this.elt.disabled = true;
        }
        return this;
      };

      self.enable = function (value) {
        if (typeof value === 'string') {
          for (let i = 0; i < this.elt.length; i++) {
            if (this.elt[i].value.toString() === value) {
              this.elt[i].disabled = false;
              this.elt[i].selected = false;
            }
          }
        } else {
          this.elt.disabled = false;
          for (let i = 0; i < this.elt.length; i++) {
            this.elt[i].disabled = false;
            this.elt[i].selected = false;
          }
        }
        return this;
      };

      return self;
    };

    /**
     * Creates a radio button element.
     *
     * The parameter is optional. If a string is passed, as in
     * `let myRadio = createSelect('food')`, then each radio option will
     * have `"food"` as its `name` parameter: `&lt;input name="food"&gt;&lt;/input&gt;`.
     * If an existing `&lt;div&gt;&lt;/div&gt;` or `&lt;span&gt;&lt;/span&gt;`
     * element is passed, as in `let myRadio = createSelect(container)`, it will
     * become the radio button's parent element.
     *
     * Radio buttons extend the <a href="#/p5.Element">p5.Element</a> class with a few
     * helpful methods for managing options:
     * - `myRadio.option(value, [label])` adds an option to the menu. The first paremeter, `value`, is a string that sets the option's value and label. The second parameter, `label`, is optional. If provided, it sets the label displayed for the `value`. If an option with `value` already exists, its label is changed and its value is returned.
     * - `myRadio.value()` returns the currently-selected option's value.
     * - `myRadio.selected()` returns the currently-selected option.
     * - `myRadio.selected(value)` selects the given option and returns it as an <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement" target="_blank">`HTMLInputElement`</a>.
     * - `myRadio.disable(shouldDisable)` enables the entire radio button if `true` is passed and disables it if `false` is passed.
     *
     * @method createRadio
     * @param  {Object} [containerElement] container HTML Element, either a `&lt;div&gt;&lt;/div&gt;`
     * or `&lt;span&gt;&lt;/span&gt;`.
     * @return {p5.Element} new <a href="#/p5.Element">p5.Element</a> object.
     *
     * @example
     * let style = document.createElement('style');
     * style.innerHTML = `
     * .p5-radio label {
     *    display: flex;
     *    align-items: center;
     *  }
     *  .p5-radio input {
     *    margin-right: 5px;
     *  }
     *  `;
     * document.head.appendChild(style);
     *
     * let myRadio;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a radio button element and place it
     *   // in the top-left corner.
     *   myRadio = createRadio();
     *   myRadio.position(0, 0);
     *   myRadio.class('p5-radio');
     *   myRadio.size(60);
     *
     *   // Add a few color options.
     *   myRadio.option('red');
     *   myRadio.option('yellow');
     *   myRadio.option('blue');
     *
     *   // Choose a default option.
     *   myRadio.selected('yellow');
     *
     *   describe('A yellow square with three color options listed, "red", "yellow", and "blue". The square changes color when the user selects a new option.');
     * }
     *
     * function draw() {
     *   // Set the background color using the radio button.
     *   let g = myRadio.value();
     *   background(g);
     * }
     *
     * @example
     * let myRadio;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a radio button element and place it
     *   // in the top-left corner.
     *   myRadio = createRadio();
     *   myRadio.position(0, 0);
     *   myRadio.size(50);
     *
     *   // Add a few color options.
     *   // Color values are labeled with
     *   // emotions they evoke.
     *   myRadio.option('red', 'love');
     *   myRadio.option('yellow', 'joy');
     *   myRadio.option('blue', 'trust');
     *
     *   // Choose a default option.
     *   myRadio.selected('yellow');
     *
     *   describe('A yellow square with three options listed, "love", "joy", and "trust". The square changes color when the user selects a new option.');
     * }
     *
     * function draw() {
     *   // Set the background color using the radio button.
     *   let c = myRadio.value();
     *   background(c);
     * }
     *
     * @example
     * let myRadio;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a radio button element and place it
     *   // in the top-left corner.
     *   myRadio = createRadio();
     *   myRadio.position(0, 0);
     *   myRadio.size(50);
     *
     *   // Add a few color options.
     *   myRadio.option('red');
     *   myRadio.option('yellow');
     *   myRadio.option('blue');
     *
     *   // Choose a default option.
     *   myRadio.selected('yellow');
     *
     *   // Create a button and place it beneath the canvas.
     *   let btn = createButton('disable');
     *   btn.position(0, 100);
     *
     *   // Call disableRadio() when btn is pressed.
     *   btn.mousePressed(disableRadio);
     *
     *   describe('A yellow square with three options listed, "red", "yellow", and "blue". The square changes color when the user selects a new option. A "disable" button beneath the canvas disables the color options when pressed.');
     * }
     *
     * function draw() {
     *   // Set the background color using the radio button.
     *   let c = myRadio.value();
     *   background(c);
     * }
     *
     * // Disable myRadio.
     * function disableRadio() {
     *   myRadio.disable(true);
     * }
     */
    /**
     * @method createRadio
     * @param {String} [name] name parameter assigned to each option's `&lt;input&gt;&lt;/input&gt;` element.
     * @return {p5.Element} new <a href="#/p5.Element">p5.Element</a> object.
     */
    /**
     * @method createRadio
     * @return {p5.Element} new <a href="#/p5.Element">p5.Element</a> object.
     */
    //counter for unique names on radio button
    let counter = 0;
    fn.createRadio = function (...args) {
      // Creates a div, adds each option as an individual input inside it.
      // If already given with a containerEl, will search for all input[radio]
      // it, create a p5.Element out of it, add options to it and return the p5.Element.

      let self;
      let radioElement;
      let name;
      const arg0 = args[0];
      if (
        arg0 instanceof Element &&
        (
          arg0.elt instanceof HTMLDivElement ||
          arg0.elt instanceof HTMLSpanElement
        )
      ) {
        // If given argument is p5.Element of div/span type
        self = arg0;
        this.elt = arg0.elt;
      } else if (
        // If existing radio Element is provided as argument 0
        arg0 instanceof HTMLDivElement ||
        arg0 instanceof HTMLSpanElement
      ) {
        self = addElement(arg0, this);
        this.elt = arg0;
        radioElement = arg0;
        if (typeof args[1] === 'string') name = args[1];
      } else {
        if (typeof arg0 === 'string') name = arg0;
        radioElement = document.createElement('div');
        self = addElement(radioElement, this);
        this.elt = radioElement;
      }
      self._name = name || `radioOption_${counter++}`;

      // setup member functions
      const isRadioInput = el =>
        el instanceof HTMLInputElement && el.type === 'radio';
      const isLabelElement = el => el instanceof HTMLLabelElement;
      const isSpanElement = el => el instanceof HTMLSpanElement;

      self._getOptionsArray = function () {
        return Array.from(this.elt.children)
          .filter(
            el =>
              isRadioInput(el) ||
              (isLabelElement(el) && isRadioInput(el.firstElementChild))
          )
          .map(el => (isRadioInput(el) ? el : el.firstElementChild));
      };

      self.option = function (value, label) {
        // return an option with this value, create if not exists.
        let optionEl;
        for (const option of self._getOptionsArray()) {
          if (option.value === value) {
            optionEl = option;
            break;
          }
        }

        // Create a new option, add it to radioElement and return it.
        if (optionEl === undefined) {
          optionEl = document.createElement('input');
          optionEl.setAttribute('type', 'radio');
          optionEl.setAttribute('value', value);
        }
        optionEl.setAttribute('name', self._name);

        // Check if label element exists, else create it
        let labelElement;
        if (!isLabelElement(optionEl.parentElement)) {
          labelElement = document.createElement('label');
          labelElement.insertAdjacentElement('afterbegin', optionEl);
        } else {
          labelElement = optionEl.parentElement;
        }

        // Check if span element exists, else create it
        let spanElement;
        if (!isSpanElement(labelElement.lastElementChild)) {
          spanElement = document.createElement('span');
          optionEl.insertAdjacentElement('afterend', spanElement);
        } else {
          spanElement = labelElement.lastElementChild;
        }

        // Set the innerHTML of span element as the label text
        spanElement.innerHTML = label === undefined ? value : label;

        // Append the label element, which includes option element and
        // span element to the radio container element
        this.elt.appendChild(labelElement);

        return optionEl;
      };

      self.remove = function (value) {
        for (const optionEl of self._getOptionsArray()) {
          if (optionEl.value === value) {
            if (isLabelElement(optionEl.parentElement)) {
              // Remove parent label which also removes children elements
              optionEl.parentElement.remove();
            } else {
              // Remove the option input if parent label does not exist
              optionEl.remove();
            }
            return;
          }
        }
      };

      self.value = function () {
        let result = '';
        for (const option of self._getOptionsArray()) {
          if (option.checked) {
            result = option.value;
            break;
          }
        }
        return result;
      };

      self.selected = function (value) {
        let result = null;
        if (value === undefined) {
          for (const option of self._getOptionsArray()) {
            if (option.checked) {
              result = option;
              break;
            }
          }
        } else {
          // forEach loop to uncheck all radio buttons before
          // setting any one as checked.
          self._getOptionsArray().forEach(option => {
            option.checked = false;
            option.removeAttribute('checked');
          });

          for (const option of self._getOptionsArray()) {
            if (option.value === value) {
              option.setAttribute('checked', true);
              option.checked = true;
              result = option;
            }
          }
        }
        return result;
      };

      self.disable = function (shouldDisable = true) {
        for (const radioInput of self._getOptionsArray()) {
          radioInput.setAttribute('disabled', shouldDisable);
        }
      };

      return self;
    };

    /**
     * Creates a color picker element.
     *
     * The parameter, `value`, is optional. If a color string or
     * <a href="#/p5.Color">p5.Color</a> object is passed, it will set the default
     * color.
     *
     * Color pickers extend the <a href="#/p5.Element">p5.Element</a> class with a
     * couple of helpful methods for managing colors:
     * - `myPicker.value()` returns the current color as a hex string in the format `'#rrggbb'`.
     * - `myPicker.color()` returns the current color as a <a href="#/p5.Color">p5.Color</a> object.
     *
     * @method createColorPicker
     * @param {String|p5.Color} [value] default color as a <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/color" target="_blank">CSS color string</a>.
     * @return {p5.Element} new <a href="#/p5.Element">p5.Element</a> object.
     *
     * @example
     * let myPicker;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a color picker and set its position.
     *   myPicker = createColorPicker('deeppink');
     *   myPicker.position(0, 100);
     *
     *   describe('A pink square with a color picker beneath it. The square changes color when the user picks a new color.');
     * }
     *
     * function draw() {
     *   // Use the color picker to paint the background.
     *   let c = myPicker.color();
     *   background(c);
     * }
     *
     * @example
     * let myPicker;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create a color picker and set its position.
     *   myPicker = createColorPicker('deeppink');
     *   myPicker.position(0, 100);
     *
     *   describe('A number with the format "#rrggbb" is displayed on a pink canvas. The background color and number change when the user picks a new color.');
     * }
     *
     * function draw() {
     *   // Use the color picker to paint the background.
     *   let c = myPicker.value();
     *   background(c);
     *
     *   // Display the current color as a hex string.
     *   text(c, 25, 55);
     * }
     */
    fn.createColorPicker = function (value) {
      // p5._validateParameters('createColorPicker', arguments);
      // TODO: This implementation needs to be rechecked or reimplemented
      // The way it worked with color is a bit too complex
      const elt = document.createElement('input');
      let self;
      elt.type = 'color';
      if (value) {
        if (value instanceof p5.Color) {
          elt.value = value.toString('#rrggbb');
        } else {
          this.push();
          this.colorMode('rgb');
          elt.value = this.color(value).toString('#rrggbb');
          this.pop();
        }
      } else {
        elt.value = '#000000';
      }
      self = addElement(elt, this);
      // Method to return a p5.Color object for the given color.
      const inst = this;
      self.color = function () {
        inst.push();
        if (value) {
          if (value.mode) {
            inst.colorMode(
              value.mode,
              ...(value?.maxes ? value.maxes[value.mode] || [] : [])
            );
          }
        }
        const c = inst.color(this.elt.value);
        inst.pop();
        return c;
      };
      return self;
    };

    /**
     * Creates a text `&lt;input&gt;&lt;/input&gt;` element.
     *
     * Call `myInput.size()` to set the length of the text box.
     *
     * The first parameter, `value`, is optional. It's a string that sets the
     * input's default value. The input is blank by default.
     *
     * The second parameter, `type`, is also optional. It's a string that
     * specifies the type of text being input. See MDN for a full
     * <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input" target="_blank">list of options</a>.
     * The default is `'text'`.
     *
     * @method createInput
     * @param {String} [value] default value of the input box. Defaults to an empty string `''`.
     * @param {String} [type] type of input. Defaults to `'text'`.
     * @return {p5.Element} new <a href="#/p5.Element">p5.Element</a> object.
     *
     * @example
     * let myInput;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create an input element and place it
     *   // beneath the canvas.
     *   myInput = createInput();
     *   myInput.position(0, 100);
     *
     *   describe('A gray square with a text box beneath it. The text in the square changes when the user types something new in the input bar.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Use the input to display a message.
     *   let msg = myInput.value();
     *   text(msg, 25, 55);
     * }
     *
     * @example
     * let myInput;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create an input element and place it
     *   // beneath the canvas. Set its default
     *   // text to "hello!".
     *   myInput = createInput('hello!');
     *   myInput.position(0, 100);
     *
     *   describe('The text "hello!" written at the center of a gray square. A text box beneath the square also says "hello!". The text in the square changes when the user types something new in the input bar.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Use the input to display a message.
     *   let msg = myInput.value();
     *   text(msg, 25, 55);
     * }
     */
    /**
     * @method createInput
     * @param {String} [value]
     * @return {p5.Element}
     */
    fn.createInput = function (value = '', type = 'text') {
      // p5._validateParameters('createInput', arguments);
      let elt = document.createElement('input');
      elt.setAttribute('value', value);
      elt.setAttribute('type', type);
      return addElement(elt, this);
    };

    /**
     * Creates an `&lt;input&gt;&lt;/input&gt;` element of type `'file'`.
     *
     * `createFileInput()` allows users to select local files for use in a sketch.
     * It returns a <a href="#/p5.File">p5.File</a> object.
     *
     * The first parameter, `callback`, is a function that's called when the file
     * loads. The callback function should have one parameter, `file`, that's a
     * <a href="#/p5.File">p5.File</a> object.
     *
     * The second parameter, `multiple`, is optional. It's a boolean value that
     * allows loading multiple files if set to `true`. If `true`, `callback`
     * will be called once per file.
     *
     * @method createFileInput
     * @param  {Function} callback function to call once the file loads.
     * @param  {Boolean} [multiple] allow multiple files to be selected.
     * @return {p5.Element} The new input element.
     *
     * @example
     * // Use the file input to select an image to
     * // load and display.
     * let input;
     * let img;
     *
     * function setup() {
     *   createCanvas(200, 200);
     *
     *   // Create a file input and place it beneath
     *   // the canvas.
     *   input = createFileInput(handleImage);
     *   input.position(0, 100);
     *
     *   describe('A gray square with a file input beneath it. If the user selects an image file to load, it is displayed on the square.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw the image if loaded.
     *   if (img) {
     *     image(img, 0, 0, width, height);
     *   }
     * }
     *
     * // Create an image if the file is an image.
     * function handleImage(file) {
     *   if (file.type === 'image') {
     *     img = createImg(file.data, '');
     *     img.hide();
     *   } else {
     *     img = null;
     *   }
     * }
     *
     * @example
     * // Use the file input to select multiple images
     * // to load and display.
     * let input;
     * let images = [];
     *
     * function setup() {
     *   // Create a file input and place it beneath
     *   // the canvas. Allow it to load multiple files.
     *   input = createFileInput(handleImage, true);
     *   input.position(0, 100);
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw the images if loaded. Each image
     *   // is drawn 20 pixels lower than the
     *   // previous image.
     *   for (let i = 0; i < images.length; i += 1) {
     *     // Calculate the y-coordinate.
     *     let y = i * 20;
     *
     *     // Draw the image.
     *     image(images[i], 0, y, 100, 100);
     *   }
     *
     *   describe('A gray square with a file input beneath it. If the user selects multiple image files to load, they are displayed on the square.');
     * }
     *
     * // Create an image if the file is an image,
     * // then add it to the images array.
     * function handleImage(file) {
     *   if (file.type === 'image') {
     *     let img = createImg(file.data, '');
     *     img.hide();
     *     images.push(img);
     *   }
     * }
     */
    fn.createFileInput = function (callback, multiple = false) {
      // p5._validateParameters('createFileInput', arguments);

      const handleFileSelect = function (event) {
        for (const file of event.target.files) {
          File._load(file, callback);
        }
      };

      // If File API's are not supported, throw Error
      if (!(window.File && window.FileReader && window.FileList && window.Blob)) {
        console.log(
          'The File APIs are not fully supported in this browser. Cannot create element.'
        );
        return;
      }

      const fileInput = document.createElement('input');
      fileInput.setAttribute('type', 'file');
      if (multiple) fileInput.setAttribute('multiple', true);
      fileInput.addEventListener('change', handleFileSelect, false);
      return addElement(fileInput, this);
    };
  }

  if(typeof p5 !== 'undefined'){
    dom$1(p5, p5.prototype);
  }

  function dom(p5){
    p5.registerAddon(dom$1);
    p5.registerAddon(element);
    p5.registerAddon(media);
    p5.registerAddon(file);
  }

  /**
   * @module Events
   * @submodule Acceleration
   * @for p5
   * @requires core
   * @main Events
   */

  function acceleration(p5, fn, lifecycles){
    lifecycles.presetup = function(){
      const events = [
        'deviceorientation',
        'devicemotion'
      ];

      for(const event of events){
        window.addEventListener(event, this[`_on${event}`].bind(this), {
          passive: false,
          signal: this._removeSignal
        });
      }
    };

    /**
     * The system variable deviceOrientation always contains the orientation of
     * the device. The value of this variable will either be set 'landscape'
     * or 'portrait'. If no data is available it will be set to 'undefined'.
     * either LANDSCAPE or PORTRAIT.
     *
     * @property {(LANDSCAPE|PORTRAIT)} deviceOrientation
     * @readOnly
     */
    fn.deviceOrientation =
      window.innerWidth / window.innerHeight > 1.0 ? 'landscape' : 'portrait';

    /**
     * The system variable accelerationX always contains the acceleration of the
     * device along the x axis. Value is represented as meters per second squared.
     *
     * @property {Number} accelerationX
     * @readOnly
     * @example
     * // Move a touchscreen device to register
     * // acceleration changes.
     * function draw() {
     *   background(220, 50);
     *   fill('magenta');
     *   ellipse(width / 2, height / 2, accelerationX);
     *   describe('Magnitude of device acceleration is displayed as ellipse size.');
     * }
     */
    fn.accelerationX = 0;

    /**
     * The system variable accelerationY always contains the acceleration of the
     * device along the y axis. Value is represented as meters per second squared.
     *
     * @property {Number} accelerationY
     * @readOnly
     * @example
     * // Move a touchscreen device to register
     * // acceleration changes.
     * function draw() {
     *   background(220, 50);
     *   fill('magenta');
     *   ellipse(width / 2, height / 2, accelerationY);
     *   describe('Magnitude of device acceleration is displayed as ellipse size');
     * }
     */
    fn.accelerationY = 0;

    /**
     * The system variable accelerationZ always contains the acceleration of the
     * device along the z axis. Value is represented as meters per second squared.
     *
     * @property {Number} accelerationZ
     * @readOnly
     *
     * @example
     * // Move a touchscreen device to register
     * // acceleration changes.
     * function draw() {
     *   background(220, 50);
     *   fill('magenta');
     *   ellipse(width / 2, height / 2, accelerationZ);
     *   describe('Magnitude of device acceleration is displayed as ellipse size');
     * }
     */
    fn.accelerationZ = 0;

    /**
     * The system variable pAccelerationX always contains the acceleration of the
     * device along the x axis in the frame previous to the current frame. Value
     * is represented as meters per second squared.
     *
     * @property {Number} pAccelerationX
     * @readOnly
     */
    fn.pAccelerationX = 0;

    /**
     * The system variable pAccelerationY always contains the acceleration of the
     * device along the y axis in the frame previous to the current frame. Value
     * is represented as meters per second squared.
     *
     * @property {Number} pAccelerationY
     * @readOnly
     */
    fn.pAccelerationY = 0;

    /**
     * The system variable pAccelerationZ always contains the acceleration of the
     * device along the z axis in the frame previous to the current frame. Value
     * is represented as meters per second squared.
     *
     * @property {Number} pAccelerationZ
     * @readOnly
     */
    fn.pAccelerationZ = 0;

    /**
     * _updatePAccelerations updates the pAcceleration values
     *
     * @private
     */
    fn._updatePAccelerations = function () {
      this.pAccelerationX = this.accelerationX;
      this.pAccelerationY = this.accelerationY;
      this.pAccelerationZ = this.accelerationZ;
    };

    /**
     * The system variable rotationX always contains the rotation of the
     * device along the x axis. If the sketch <a href="#/p5/angleMode">
     * angleMode()</a> is set to DEGREES, the value will be -180 to 180. If
     * it is set to RADIANS, the value will be -PI to PI.
     *
     * Note: The order the rotations are called is important, ie. if used
     * together, it must be called in the order Z-X-Y or there might be
     * unexpected behaviour.
     *
     * @property {Number} rotationX
     * @readOnly
     * @example
     * let rotationX = 0;            // Angle in degrees
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);   // Create 3D canvas
     * }
     *
     * function draw() {
     *   background(220);                 // Set light gray background
     *   rotateX(radians(rotationX));     // Rotate around X-axis
     *   normalMaterial();                // Apply simple shaded material
     *   box(60);                         // Draw 3D cube (60 units wide)
     *   rotationX = (rotationX + 2) % 360; // Increment rotation (2° per frame)
     * }
     */
    fn.rotationX = 0;

    /**
     * The system variable rotationY always contains the rotation of the
     * device along the y axis. If the sketch <a href="#/p5/angleMode">
     * angleMode()</a> is set to DEGREES, the value will be -90 to 90. If
     * it is set to RADIANS, the value will be -PI/2 to PI/2.
     *
     * Note: The order the rotations are called is important, ie. if used
     * together, it must be called in the order Z-X-Y or there might be
     * unexpected behaviour.
     *
     * @property {Number} rotationY
     * @readOnly
     * @example
     * let rotationY = 0;            // Angle in degrees
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);   // Create 3D canvas
     * }
     *
     * function draw() {
     *   background(220);                 // Set light gray background
     *   rotateY(radians(rotationY));     // Rotate around Y-axis (vertical)
     *   normalMaterial();                // Apply simple shaded material
     *   box(60);                         // Draw 3D cube (60 units wide)
     *   rotationY = (rotationY + 2) % 360; // Increment rotation (2° per frame)
     * }
     */
    fn.rotationY = 0;

    /**
     * The system variable rotationZ always contains the rotation of the
     * device along the z axis. If the sketch <a href="#/p5/angleMode">
     * angleMode()</a> is set to DEGREES, the value will be 0 to 360. If
     * it is set to RADIANS, the value will be 0 to 2*PI.
     *
     * Unlike rotationX and rotationY, this variable is available for devices
     * with a built-in compass only.
     *
     * Note: The order the rotations are called is important, ie. if used
     * together, it must be called in the order Z-X-Y or there might be
     * unexpected behaviour.
     *
     * @example
     * let rotationZ = 0;          // Angle in degrees
     *
     * function setup() {
     *   createCanvas(200, 200, WEBGL);   // Create 3D canvas
     * }
     *
     * function draw() {
     *   background(220);
     *   rotateZ(radians(rotationZ));     // Rotate around Z-axis
     *   normalMaterial();                // Apply simple shaded material
     *   box(60);                         // Draw 3D cube
     *   rotationZ = (rotationZ + 2) % 360; // Increment rotation angle
     * }
     *
     * @property {Number} rotationZ
     * @readOnly
     */
    fn.rotationZ = 0;

    /**
     * The system variable pRotationX always contains the rotation of the
     * device along the x axis in the frame previous to the current frame.
     * If the sketch <a href="#/p5/angleMode"> angleMode()</a> is set to DEGREES,
     * the value will be -180 to 180. If it is set to RADIANS, the value will
     * be -PI to PI.
     *
     * pRotationX can also be used with rotationX to determine the rotate
     * direction of the device along the X-axis.
     * @example
     * // META:norender
     * // A simple if statement looking at whether
     * // rotationX - pRotationX < 0 is true or not will be
     * // sufficient for determining the rotate direction
     * // in most cases.
     *
     * // Some extra logic is needed to account for cases where
     * // the angles wrap around.
     * let rotateDirection = 'clockwise';
     *
     * // Simple range conversion to make things simpler.
     * // This is not absolutely necessary but the logic
     * // will be different in that case.
     *
     * let rX = rotationX + 180;
     * let pRX = pRotationX + 180;
     *
     * if ((rX - pRX > 0 && rX - pRX < 270) || rX - pRX < -270) {
     *   rotateDirection = 'clockwise';
     * } else if (rX - pRX < 0 || rX - pRX > 270) {
     *   rotateDirection = 'counter-clockwise';
     * }
     *
     * print(rotateDirection);
     * describe('no image to display.');
     *
     * @property {Number} pRotationX
     * @readOnly
     */
    fn.pRotationX = 0;

    /**
     * The system variable pRotationY always contains the rotation of the
     * device along the y axis in the frame previous to the current frame.
     * If the sketch <a href="#/p5/angleMode"> angleMode()</a> is set to DEGREES,
     * the value will be -90 to 90. If it is set to RADIANS, the value will
     * be -PI/2 to PI/2.
     *
     * pRotationY can also be used with rotationY to determine the rotate
     * direction of the device along the Y-axis.
     * @example
     * // META:norender
     * // A simple if statement looking at whether
     * // rotationY - pRotationY < 0 is true or not will be
     * // sufficient for determining the rotate direction
     * // in most cases.
     *
     * // Some extra logic is needed to account for cases where
     * // the angles wrap around.
     * let rotateDirection = 'clockwise';
     *
     * // Simple range conversion to make things simpler.
     * // This is not absolutely necessary but the logic
     * // will be different in that case.
     *
     * let rY = rotationY + 180;
     * let pRY = pRotationY + 180;
     *
     * if ((rY - pRY > 0 && rY - pRY < 270) || rY - pRY < -270) {
     *   rotateDirection = 'clockwise';
     * } else if (rY - pRY < 0 || rY - pRY > 270) {
     *   rotateDirection = 'counter-clockwise';
     * }
     * print(rotateDirection);
     * describe('no image to display.');
     *
     * @property {Number} pRotationY
     * @readOnly
     */
    fn.pRotationY = 0;

    /**
     * The system variable pRotationZ always contains the rotation of the
     * device along the z axis in the frame previous to the current frame.
     * If the sketch <a href="#/p5/angleMode"> angleMode()</a> is set to DEGREES,
     * the value will be 0 to 360. If it is set to RADIANS, the value will
     * be 0 to 2*PI.
     *
     * pRotationZ can also be used with rotationZ to determine the rotate
     * direction of the device along the Z-axis.
     * @example
     * // META:norender
     * // A simple if statement looking at whether
     * // rotationZ - pRotationZ < 0 is true or not will be
     * // sufficient for determining the rotate direction
     * // in most cases.
     *
     * // Some extra logic is needed to account for cases where
     * // the angles wrap around.
     * let rotateDirection = 'clockwise';
     *
     * if (
     *   (rotationZ - pRotationZ > 0 && rotationZ - pRotationZ < 270) ||
     *   rotationZ - pRotationZ < -270
     * ) {
     *   rotateDirection = 'clockwise';
     * } else if (rotationZ - pRotationZ < 0 || rotationZ - pRotationZ > 270) {
     *   rotateDirection = 'counter-clockwise';
     * }
     * print(rotateDirection);
     * describe('no image to display.');
     *
     * @property {Number} pRotationZ
     * @readOnly
     */
    fn.pRotationZ = 0;

    let startAngleX = 0;
    let startAngleY = 0;
    let startAngleZ = 0;

    let rotateDirectionX = 'clockwise';
    let rotateDirectionY = 'clockwise';
    let rotateDirectionZ = 'clockwise';

    fn.pRotateDirectionX = undefined;
    fn.pRotateDirectionY = undefined;
    fn.pRotateDirectionZ = undefined;

    fn._updatePRotations = function () {
      this.pRotationX = this.rotationX;
      this.pRotationY = this.rotationY;
      this.pRotationZ = this.rotationZ;
    };

    /**
     * When a device is rotated, the axis that triggers the <a href="#/p5/deviceTurned">deviceTurned()</a>
     * method is stored in the turnAxis variable. The turnAxis variable is only defined within
     * the scope of deviceTurned().
     * @property {String} turnAxis
     * @readOnly
     * @example
     * // Run this example on a mobile device
     * // Rotate the device by 90 degrees in the
     * // X-axis to change the value.
     *
     * let value = 0;
     * function draw() {
     *   fill(value);
     *   rect(25, 25, 50, 50);
     *   describe(`50-by-50 black rect in center of canvas.
     *     turns white on mobile when device turns`);
     *   describe(`50-by-50 black rect in center of canvas.
     *     turns white on mobile when x-axis turns`);
     * }
     * function deviceTurned() {
     *   if (turnAxis === 'X') {
     *     if (value === 0) {
     *       value = 255;
     *     } else if (value === 255) {
     *       value = 0;
     *     }
     *   }
     * }
     */
    fn.turnAxis = undefined;

    let move_threshold = 0.5;
    let shake_threshold = 30;

    /**
     * The <a href="#/p5/setMoveThreshold">setMoveThreshold()</a> function is used to set the movement threshold for
     * the <a href="#/p5/deviceMoved">deviceMoved()</a> function. The default threshold is set to 0.5.
     *
     * @method setMoveThreshold
     * @param {Number} value The threshold value
     * @example
     * // META:norender
     * // Run this example on a mobile device
     * // You will need to move the device incrementally further
     * // the closer the square's color gets to white in order to change the value.
     *
     * let value = 0;
     * let threshold = 0.5;
     * function setup() {
     *   setMoveThreshold(threshold);
     * }
     * function draw() {
     *   fill(value);
     *   rect(25, 25, 50, 50);
     *   describe(`50-by-50 black rect in center of canvas.
     *     turns white on mobile when device moves`);
     * }
     * function deviceMoved() {
     *   value = value + 5;
     *   threshold = threshold + 0.1;
     *   if (value > 255) {
     *     value = 0;
     *     threshold = 30;
     *   }
     *   setMoveThreshold(threshold);
     * }
     */
    fn.setMoveThreshold = function (val) {
      move_threshold = val;
    };

    /**
     * The <a href="#/p5/setShakeThreshold">setShakeThreshold()</a> function is used to set the movement threshold for
     * the <a href="#/p5/deviceShaken">deviceShaken()</a> function. The default threshold is set to 30.
     *
     * @method setShakeThreshold
     * @param {Number} value The threshold value
     * @example
     * // META:norender
     * // Run this example on a mobile device
     * // You will need to shake the device more firmly
     * // the closer the box's fill gets to white in order to change the value.
     *
     * let value = 0;
     * let threshold = 30;
     * function setup() {
     *   setShakeThreshold(threshold);
     * }
     * function draw() {
     *   fill(value);
     *   rect(25, 25, 50, 50);
     *   describe(`50-by-50 black rect in center of canvas.
     *     turns white on mobile when device is being shaked`);
     * }
     * function deviceMoved() {
     *   value = value + 5;
     *   threshold = threshold + 5;
     *   if (value > 255) {
     *     value = 0;
     *     threshold = 30;
     *   }
     *   setShakeThreshold(threshold);
     * }
     */
    fn.setShakeThreshold = function (val) {
      shake_threshold = val;
    };

    /**
     * The <a href="#/p5/deviceMoved">deviceMoved()</a> function is called when the device is moved by more than
     * the threshold value along X, Y or Z axis. The default threshold is set to 0.5.
     * The threshold value can be changed using <a href="https://p5js.org/reference/p5/setMoveThreshold">setMoveThreshold()</a>.
     *
     * @method deviceMoved
     * @example
     * // META:norender
     * // Run this example on a mobile device
     * // Move the device around
     * // to change the value.
     *
     * let value = 0;
     * function draw() {
     *   fill(value);
     *   rect(25, 25, 50, 50);
     *   describe(`50-by-50 black rect in center of canvas.
     *     turns white on mobile when device moves`);
     * }
     * function deviceMoved() {
     *   value = value + 5;
     *   if (value > 255) {
     *     value = 0;
     *   }
     * }
     */

    /**
     * The <a href="#/p5/deviceTurned">deviceTurned()</a> function is called when the device rotates by
     * more than 90 degrees continuously.
     *
     * The axis that triggers the <a href="#/p5/deviceTurned">deviceTurned()</a> method is stored in the turnAxis
     * variable. The <a href="#/p5/deviceTurned">deviceTurned()</a> method can be locked to trigger on any axis:
     * X, Y or Z by comparing the turnAxis variable to 'X', 'Y' or 'Z'.
     *
     * @method deviceTurned
     * @example
     * // META:norender
     * // Run this example on a mobile device
     * // Rotate the device by 90 degrees
     * // to change the value.
     *
     * let value = 0;
     * function draw() {
     *   fill(value);
     *   rect(25, 25, 50, 50);
     *   describe(`50-by-50 black rect in center of canvas.
     *     turns white on mobile when device turns`);
     * }
     * function deviceTurned() {
     *   if (value === 0) {
     *     value = 255;
     *   } else if (value === 255) {
     *     value = 0;
     *   }
     * }
     *
     * @example
     * // Run this example on a mobile device
     * // Rotate the device by 90 degrees in the
     * // X-axis to change the value.
     *
     * let value = 0;
     * function draw() {
     *   fill(value);
     *   rect(25, 25, 50, 50);
     *   describe(`50-by-50 black rect in center of canvas.
     *     turns white on mobile when x-axis turns`);
     * }
     * function deviceTurned() {
     *   if (turnAxis === 'X') {
     *     if (value === 0) {
     *       value = 255;
     *     } else if (value === 255) {
     *       value = 0;
     *     }
     *   }
     * }
     */

    /**
     * The <a href="#/p5/deviceShaken">deviceShaken()</a> function is called when the device total acceleration
     * changes of accelerationX and accelerationY values is more than
     * the threshold value. The default threshold is set to 30.
     * The threshold value can be changed using <a href="https://p5js.org/reference/p5/setShakeThreshold">setShakeThreshold()</a>.
     *
     * @method deviceShaken
     * @example
     * // META:norender
     * // Run this example on a mobile device
     * // Shake the device to change the value.
     *
     * let value = 0;
     * function draw() {
     *   fill(value);
     *   rect(25, 25, 50, 50);
     *   describe(`50-by-50 black rect in center of canvas.
     *     turns white on mobile when device shakes`);
     * }
     *
     * function deviceShaken() {
     *   value = value + 5;
     *   if (value > 255) {
     *     value = 0;
     *   }
     * }
     */
    fn._ondeviceorientation = function (e) {
      this._updatePRotations();

      // Convert from degrees into current angle mode
      this.rotationX = this._fromDegrees(e.beta);
      this.rotationY = this._fromDegrees(e.gamma);
      this.rotationZ = this._fromDegrees(e.alpha);
      this._handleMotion();
    };

    fn._ondevicemotion = function (e) {
      this._updatePAccelerations();
      this.accelerationX = e.acceleration.x * 2;
      this.accelerationY = e.acceleration.y * 2;
      this.accelerationZ = e.acceleration.z * 2;
      this._handleMotion();
    };

    fn._handleMotion = function () {
      if (
        screen.orientation.type === 'landscape-primary' ||
        screen.orientation.type === 'landscape-secondary'
      ) {
        this.deviceOrientation = 'landscape';
      } else if (
        screen.orientation.type === 'portrait-primary' ||
        screen.orientation.type === 'portrait-secondary'
      ) {
        this.deviceOrientation = 'portrait';
      } else {
        this.deviceOrientation = 'undefined';
      }

      if (typeof this._customActions.deviceMoved === 'function') {
        if (
          Math.abs(this.accelerationX - this.pAccelerationX) > move_threshold ||
          Math.abs(this.accelerationY - this.pAccelerationY) > move_threshold ||
          Math.abs(this.accelerationZ - this.pAccelerationZ) > move_threshold
        ) {
          this._customActions.deviceMoved();
        }
      }

      if (typeof this._customActions.deviceTurned === 'function') {
        // The angles given by rotationX etc is from range [-180 to 180].
        // The following will convert them to [0 to 360] for ease of calculation
        // of cases when the angles wrapped around.
        // _startAngleX will be converted back at the end and updated.

        // Rotations are converted to degrees and all calculations are done in degrees
        const wRX = this._toDegrees(this.rotationX) + 180;
        const wPRX = this._toDegrees(this.pRotationX) + 180;
        let wSAX = startAngleX + 180;
        if ((wRX - wPRX > 0 && wRX - wPRX < 270) || wRX - wPRX < -270) {
          rotateDirectionX = 'clockwise';
        } else if (wRX - wPRX < 0 || wRX - wPRX > 270) {
          rotateDirectionX = 'counter-clockwise';
        }
        if (rotateDirectionX !== this.pRotateDirectionX) {
          wSAX = wRX;
        }
        if (Math.abs(wRX - wSAX) > 90 && Math.abs(wRX - wSAX) < 270) {
          wSAX = wRX;
          this.turnAxis = 'X';
          this._customActions.deviceTurned();
        }
        this.pRotateDirectionX = rotateDirectionX;
        startAngleX = wSAX - 180;

        // Y-axis is identical to X-axis except for changing some names.
        const wRY = this._toDegrees(this.rotationY) + 180;
        const wPRY = this._toDegrees(this.pRotationY) + 180;
        let wSAY = startAngleY + 180;
        if ((wRY - wPRY > 0 && wRY - wPRY < 270) || wRY - wPRY < -270) {
          rotateDirectionY = 'clockwise';
        } else if (wRY - wPRY < 0 || wRY - this.pRotationY > 270) {
          rotateDirectionY = 'counter-clockwise';
        }
        if (rotateDirectionY !== this.pRotateDirectionY) {
          wSAY = wRY;
        }
        if (Math.abs(wRY - wSAY) > 90 && Math.abs(wRY - wSAY) < 270) {
          wSAY = wRY;
          this.turnAxis = 'Y';
          this._customActions.deviceTurned();
        }
        this.pRotateDirectionY = rotateDirectionY;
        startAngleY = wSAY - 180;

        // Z-axis is already in the range 0 to 360
        // so no conversion is needed.
        const rotZ = this._toDegrees(this.rotationZ);
        const pRotZ = this._toDegrees(this.pRotationZ);
        if (
          (rotZ - pRotZ > 0 && rotZ - pRotZ < 270) ||
          rotZ - pRotZ < -270
        ) {
          rotateDirectionZ = 'clockwise';
        } else if (
          rotZ - pRotZ < 0 ||
          rotZ - pRotZ > 270
        ) {
          rotateDirectionZ = 'counter-clockwise';
        }
        if (rotateDirectionZ !== this.pRotateDirectionZ) {
          startAngleZ = rotZ;
        }
        if (
          Math.abs(rotZ - startAngleZ) > 90 &&
          Math.abs(rotZ - startAngleZ) < 270
        ) {
          startAngleZ = rotZ;
          this.turnAxis = 'Z';
          this._customActions.deviceTurned();
        }
        this.pRotateDirectionZ = rotateDirectionZ;
        this.turnAxis = undefined;
      }
      if (typeof this._customActions.deviceShaken === 'function') {
        let accelerationChangeX;
        let accelerationChangeY;
        // Add accelerationChangeZ if acceleration change on Z is needed
        if (this.pAccelerationX !== null) {
          accelerationChangeX = Math.abs(
            this.accelerationX - this.pAccelerationX
          );
          accelerationChangeY = Math.abs(
            this.accelerationY - this.pAccelerationY
          );
        }
        if (accelerationChangeX + accelerationChangeY > shake_threshold) {
          this._customActions.deviceShaken();
        }
      }
    };
  }

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

  /**
   * @module Events
   * @submodule Keyboard
   * @for p5
   * @requires core
   */
  function isCode(input) {
    const leftRightKeys = [
      'Alt',
      'Shift',
      'Control',
      'Meta'
    ];
    if (leftRightKeys.includes(input)) {
      return false;
    }
    if (typeof input !== 'string') {
      return false;
    }
    return input.length > 1;
  }

  function keyboard(p5, fn, lifecycles){
    lifecycles.presetup = function(){
      const events = [
        'keydown',
        'keyup',
        'keypress',
        'blur'
      ];

      for(const event of events){
        window.addEventListener(event, this[`_on${event}`].bind(this), {
          passive: false,
          signal: this._removeSignal
        });
      }
    };

    /**
     * A `Boolean` system variable that's `true` if any key is currently pressed
     * and `false` if not.
     *
     * @property {Boolean} keyIsPressed
     * @readOnly
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a white square at its center. The white square turns black when the user presses a key.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the square.
     *   if (keyIsPressed === true) {
     *     fill(0);
     *   } else {
     *     fill(255);
     *   }
     *
     *   // Draw the square.
     *   square(25, 25, 50);
     * }
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a white square at its center. The white square turns black when the user presses a key.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the square.
     *   if (keyIsPressed) {
     *     fill(0);
     *   } else {
     *     fill(255);
     *   }
     *
     *   // Draw the square.
     *   square(25, 25, 50);
     * }
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with the word "false" at its center. The word switches to "true" when the user presses a key.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the value of keyIsPressed.
     *   text(keyIsPressed, 50, 50);
     * }
     */
    fn.keyIsPressed = false;

    /**
     * A `String` system variable that contains the value of the last key typed.
     *
     * The key variable is helpful for checking whether an
     * <a href="https://en.wikipedia.org/wiki/ASCII#Printable_characters" target="_blank">ASCII</a>
     * key has been typed. For example, the expression `key === "a"` evaluates to
     * `true` if the `a` key was typed and `false` if not. `key` doesn’t update
     * for special keys such as `LEFT_ARROW` and `ENTER`. Use keyCode instead for
     * special keys. The <a href="#/p5/keyIsDown">keyIsDown()</a> function should
     * be used to check for multiple different key presses at the same time.
     *
     * @property {String} key
     * @readOnly
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square. The last key pressed is displayed at the center.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the last key pressed.
     *   text(key, 50, 50);
     * }
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     *
     * let x = 50;
     * let y = 50;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   describe(
     *     'A gray square with a black circle at its center. The circle moves when the user presses the keys "w", "a", "s", or "d". It leaves a trail as it moves.'
     *   );
     * }
     *
     * function draw() {
     *   // Update x and y if a key is pressed.
     *   if (keyIsPressed === true) {
     *     if (key === 'w') {
     *       y -= 1;
     *     } else if (key === 's') {
     *       y += 1;
     *     } else if (key === 'a') {
     *       x -= 1;
     *     } else if (key === 'd') {
     *       x += 1;
     *     }
     *   }
     *
     *   // Style the circle.
     *   fill(0);
     *
     *   // Draw the circle at (x, y).
     *   circle(x, y, 5);
     * }
     */
    fn.key = '';

    /**
     * The `code` property represents a physical key on the keyboard (as opposed
     * to the character generated by pressing the key). In other words, this
     * property returns a value that isn't altered by keyboard layout or the state
     * of the modifier keys.
     *
     * This property is useful when you want to handle keys based on their
     * physical positions on the input device rather than the characters associated
     * with those keys;
     *
     * Unlike <a href="#/p5/key">key</a>, the `code` property differentiates between
     * physical keys that generate the same character—for example, `CtrlLeft` and
     * `CtrlRight`—so each can be handled independently.
     * Here's the MDN docs for <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code" target="_blank">KeyboardEvent.code</a>
     *
     *
     * Pressing the key physically labeled “A” always yields `KeyA`, regardless
     * of the current keyboard layout (QWERTY, Dvorak, AZERTY, etc.) or the character
     * that appears in a text field.
     *
     * The code property returns a plain string (e.g., 'ArrowRight'). You can
     * compare it directly with string literals:
     * ```js
     * if (keyIsDown(RIGHT_ARROW)) {
     *   // …
     * }
     * // The line above is equivalent to:
     * if (code === 'ArrowRight') {
     *   // …
     * }
     * if (key  === 'ArrowRight') {
     * // …
     * }
     * ```
     *
     * The system variables `BACKSPACE`, `DELETE`, `ENTER`, `RETURN`, `TAB`,
     * `ESCAPE`, `SHIFT`, `CONTROL`, `OPTION`, `ALT`, `UP_ARROW`, `DOWN_ARROW`,
     * `LEFT_ARROW`, and `RIGHT_ARROW` are all helpful shorthands the key codes of
     * special keys.
     * These are simply shorthands for the same string values:
     * ```js
     * if (code === RIGHT_ARROW) {
     * // ..
     * }
     * ```
     *
     *
     * <p>The table below summarizes how the main keyboard-related system variables changed between p5.js 1.x and 2.x.</p>
     * <table>
     *   <thead>
     *     <tr>
     *       <th>Variable</th>
     *       <th>p5.js 1.x </th>
     *       <th>p5.js 2.x </th>
     *     </tr>
     *   </thead>
     *   <tbody>
     *     <tr>
     *       <td><code>key</code></td>
     *       <td>Text string (e.g., <code>"ArrowUp"</code>).</td>
     *       <td>Text string (e.g., <code>"ArrowUp"</code>, <code>"f"</code> or <code>"F"</code>).</td>
     *     </tr>
     *     <tr>
     *       <td><code>code</code></td>
     *       <td><em>Not supported.</em></td>
     *       <td>Text String (e.g., <code>"ArrowUp"</code>, <code>"KeyF"</code>).</td>
     *     </tr>
     *     <tr>
     *       <td><code>keyCode</code></td>
     *       <td>Number (e.g., <code>70</code>).</td>
     *       <td>Number (unchanged; e.g., <code>70</code>).</td>
     *     </tr>
     *     <tr>
     *       <td>System variables (<code>BACKSPACE</code>, <code>UP_ARROW</code>, …)</td>
     *       <td>Number</td>
     *       <td>Text String (e.g., <code>"ArrowUp"</code>).</td>
     *     </tr>
     *   </tbody>
     * </table>
     *
     *
     * @property {String} code
     * @readOnly
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square. The last key pressed is displayed at the center.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the last key pressed.
     *   text(code, 50, 50);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     * }
     *
     * function draw() {
     *   background(220);
     *   fill("black");
     *   if (keyIsDown(BACKSPACE) || keyIsDown(ENTER) ||
     *       keyIsDown(DELETE) || keyIsDown(RETURN) ||
     *       keyIsDown(TAB) || keyIsDown(ESCAPE) ||
     *       keyIsDown(CONTROL) || keyIsDown(OPTION) ||
     *       keyIsDown(UP_ARROW) || keyIsDown(LEFT_ARROW) ||
     *       keyIsDown(RIGHT_ARROW) || keyIsDown(DOWN_ARROW) ||
     *       keyIsDown(SHIFT)) {
     *     fill("red");
     *     text("System Variable", 7, 75);
     *   }
     *
     *   text(key, 30, 25);
     *   text(keyCode, 7, 25);
     *   text(code || " ", 30, 50);
     * }
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     *
     * let x = 50;
     * let y = 50;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   describe(
     *     'A gray square with a black circle at its center. The circle moves when the user presses an arrow key. It leaves a trail as it moves.'
     *   );
     * }
     *
     * function draw() {
     *   // Update x and y if an arrow key is pressed.
     *   if (keyIsPressed){
     *     if (keyIsDown(LEFT_ARROW)){
     *       x -= 1;
     *     }
     *
     *     if (keyIsDown(RIGHT_ARROW)) {
     *       x += 1;
     *     }
     *
     *     if (keyIsDown(UP_ARROW)) {
     *       y -= 1;
     *     }
     *
     *     if (keyIsDown(DOWN_ARROW)) {
     *       y += 1;
     *     }
     *   }
     *
     *   // Style the circle.
     *   fill(0);
     *
     *   // Draw the circle.
     *   circle(x, y, 5);
     * }
     */
    fn.code = '';

    /**
     * A `Number` system variable that contains the code of the last key pressed.
     *
     * Every key has a numeric key code. For example, the letter `a` key has the key code 65.
     * Use this key code to determine which key was pressed by comparing it to the numeric value
     * of the desired key.
     *
     * For example, to detect when the Enter key is pressed:
     *
     * ```js
     * if (keyCode === 13) { // Enter key
     *   // Code to run if the Enter key was pressed.
     * }
     * ```
     *
     * Alternatively, you can use the <a href="#/p5/key">key</a> function to directly compare the key value:
     *
     * ```js
     * if (key === 'Enter') { // Enter key
     *   // Code to run if the Enter key was pressed.
     * }
     * ```
     *
     * Use the following numeric codes for the arrow keys:
     *
     *   Up Arrow: 38
     *   Down Arrow: 40
     *   Left Arrow: 37
     *   Right Arrow: 39
     *
     * More key codes can be found at websites such as
     * <a href="http://keycode.info/">keycode.info</a>.
     *
     * @property {Integer} keyCode
     * @readOnly
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square. The last key pressed and its code are displayed at the center.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the last key pressed and its code.
     *   text(`${key} : ${keyCode}`, 50, 50);
     * }
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     *
     * let x = 50;
     * let y = 50;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   describe(
     *     'A gray square with a black circle at its center. The circle moves when the user presses an arrow key. It leaves a trail as it moves.'
     *   );
     * }
     *
     * function draw() {
     *   // Update x and y if an arrow key is pressed.
     *   if (keyIsPressed === true) {
     *     if (keyCode === 38) { // Up arrow key
     *       y -= 1;
     *     } else if (keyCode === 40) { // Down arrow key
     *       y += 1;
     *     } else if (keyCode === 37) { // Left arrow key
     *       x -= 1;
     *     } else if (keyCode === 39) { // Right arrow key
     *       x += 1;
     *     }
     *   }
     *
     *   // Style the circle.
     *   fill(0);
     *
     *   // Draw the circle at (x, y).
     *   circle(x, y, 5);
     * }
     */
    fn.keyCode = 0;

    /**
     * A function that's called once when any key is pressed.
     *
     * Declaring the function `keyPressed()` sets a code block to run once
     * automatically when the user presses any key:
     *
     * ```js
     * function keyPressed() {
     *   // Code to run.
     * }
     * ```
     *
     * The <a href="#/p5/key">key</a> and <a href="#/p5/keyCode">keyCode</a>
     * variables will be updated with the most recently typed value when
     * `keyPressed()` is called by p5.js:
     *
     * ```js
     * function keyPressed() {
     *   if (key === 'c') {
     *     // Code to run.
     *   }
     *
     *   if (keyCode === 13) { // Enter key
     *     // Code to run.
     *   }
     * }
     * ```
     *
     * The parameter, `event`, is optional. `keyPressed()` is always passed a
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent" target="_blank">KeyboardEvent</a>
     * object with properties that describe the key press event:
     *
     * ```js
     * function keyPressed(event) {
     *   // Code to run that uses the event.
     *   console.log(event);
     * }
     * ```
     *
     * Browsers may have default behaviors attached to various key events. For
     * example, some browsers may jump to the bottom of a web page when the
     * `SPACE` key is pressed. To prevent any default behavior for this event, add
     * `return false;` to the end of the function.
     *
     * @method keyPressed
     * @param  {KeyboardEvent} [event] optional `KeyboardEvent` callback argument.
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     *
     * let value = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a black square at its center. The inner square changes color when the user presses a key.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the square.
     *   fill(value);
     *
     *   // Draw the square.
     *   square(25, 25, 50);
     * }
     *
     * // Toggle the background color when the user presses a key.
     * function keyPressed() {
     *   if (value === 0) {
     *     value = 255;
     *   } else {
     *     value = 0;
     *   }
     *   // Uncomment to prevent any default behavior.
     *   // return false;
     * }
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     *
     * let value = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a white square at its center. The inner square turns black when the user presses the "b" key. It turns white when the user presses the "a" key.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the square.
     *   fill(value);
     *
     *   // Draw the square.
     *   square(25, 25, 50);
     * }
     *
     * // Reassign value when the user presses the 'a' or 'b' key.
     * function keyPressed() {
     *   if (key === 'a') {
     *     value = 255;
     *   } else if (key === 'b') {
     *     value = 0;
     *   }
     *   // Uncomment to prevent any default behavior.
     *   // return false;
     * }
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     *
     * let value = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a black square at its center. The inner square turns white when the user presses the left arrow key. It turns black when the user presses the right arrow key.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the square.
     *   fill(value);
     *
     *   // Draw the square.
     *   square(25, 25, 50);
     * }
     *
     * // Toggle the background color when the user presses an arrow key.
     * function keyPressed() {
     *   if (keyCode === 37) { // Left arrow key
     *     value = 255;
     *   } else if (keyCode === 39) { // Right arrow key
     *     value = 0;
     *   }
     *   // Uncomment to prevent any default behavior.
     *   // return false;
     * }
     */
    fn._onkeydown = function(e) {
      if (this._downKeys[e.code]) {
        return;
      }

      this.keyIsPressed = true;
      this.keyCode = e.which;
      this.key = e.key;
      this.code = e.code;
      this._downKeyCodes[e.code] = true;
      this._downKeys[e.key] = true;

      if (typeof this._customActions.keyPressed === 'function' && !e.charCode) {
        const executeDefault = this._customActions.keyPressed(e);
        if (executeDefault === false) {
          e.preventDefault();
        }
      }
    };

    /**
     * A function that's called once when any key is released.
     *
     * Declaring the function `keyReleased()` sets a code block to run once
     * automatically when the user releases any key:
     *
     * ```js
     * function keyReleased() {
     *   // Code to run.
     * }
     * ```
     *
     * The <a href="#/p5/key">key</a> and <a href="#/p5/keyCode">keyCode</a>
     * variables will be updated with the most recently released value when
     * `keyReleased()` is called by p5.js:
     *
     * ```js
     * function keyReleased() {
     *   if (key === 'c') {
     *     // Code to run.
     *   }
     *
     *   if (keyCode === 13) { // Enter key
     *     // Code to run.
     *   }
     * }
     * ```
     *
     * The parameter, `event`, is optional. `keyReleased()` is always passed a
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent" target="_blank">KeyboardEvent</a>
     * object with properties that describe the key press event:
     *
     * ```js
     * function keyReleased(event) {
     *   // Code to run that uses the event.
     *   console.log(event);
     * }
     * ```
     *
     * Browsers may have default behaviors attached to various key events. To
     * prevent any default behavior for this event, add `return false;` to the end
     * of the function.
     *
     * @method keyReleased
     * @param  {KeyboardEvent} [event] optional `KeyboardEvent` callback argument.
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     *
     * let value = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a black square at its center. The inner square changes color when the user releases a key.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the square.
     *   fill(value);
     *
     *   // Draw the square.
     *   square(25, 25, 50);
     * }
     *
     * // Toggle value when the user releases a key.
     * function keyReleased() {
     *   if (value === 0) {
     *     value = 255;
     *   } else {
     *     value = 0;
     *   }
     *   // Uncomment to prevent any default behavior.
     *   // return false;
     * }
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     *
     * let value = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a black square at its center. The inner square becomes white when the user releases the "w" key.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the square.
     *   fill(value);
     *
     *   // Draw the square.
     *   square(25, 25, 50);
     * }
     *
     * // Set value to 255 the user releases the 'w' key.
     * function keyReleased() {
     *   if (key === 'w') {
     *     value = 255;
     *   }
     *   // Uncomment to prevent any default behavior.
     *   // return false;
     * }
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     *
     * let value = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a black square at its center. The inner square turns white when the user presses and releases the left arrow key. It turns black when the user presses and releases the right arrow key.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the square.
     *   fill(value);
     *
     *   // Draw the square.
     *   square(25, 25, 50);
     * }
     *
     * // Toggle the background color when the user releases an arrow key.
     * function keyReleased() {
     *   if (keyCode === 37) { // Left arrow key
     *     value = 255;
     *   } else if (keyCode === 39) { // Right arrow key
     *     value = 0;
     *   }
     *   // Uncomment to prevent any default behavior.
     *   // return false;
     * }
     */
    fn._onkeyup = function(e) {
      if (typeof this._customActions.keyReleased === 'function') {
        const executeDefault = this._customActions.keyReleased(e);
        if (executeDefault === false) {
          e.preventDefault();
        }
      }

      delete this._downKeyCodes[e.code];
      delete this._downKeys[e.key];


      if (!this._areDownKeys()) {
        this.keyIsPressed = false;
        this.key = '';
        this.code = '';
      } else {
        // If other keys are still pressed, update code to the last pressed key
        const lastPressedCode = Object.keys(this._downKeyCodes).pop();
        this.code = lastPressedCode;
        const lastPressedKey = Object.keys(this._downKeys).pop();
        this.key = lastPressedKey;
      }

    };

    /**
     * A function that's called once when keys with printable characters are pressed.
     *
     * Declaring the function `keyTyped()` sets a code block to run once
     * automatically when the user presses any key with a printable character such
     * as `a` or 1. Modifier keys such as `SHIFT`, `CONTROL`, and the arrow keys
     * will be ignored:
     *
     * ```js
     * function keyTyped() {
     *   // Code to run.
     * }
     * ```
     *
     * The <a href="#/p5/key">key</a> and <a href="#/p5/keyCode">keyCode</a>
     * variables will be updated with the most recently released value when
     * `keyTyped()` is called by p5.js:
     *
     * ```js
     * function keyTyped() {
     *   // Check for the "c" character using key.
     *   if (key === 'c') {
     *     // Code to run.
     *   }
     *
     *   // Check for "c" using keyCode.
     *   if (keyCode === 67) { // 67 is the ASCII code for 'c'
     *     // Code to run.
     *   }
     * }
     * ```
     *
     * The parameter, `event`, is optional. `keyTyped()` is always passed a
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent" target="_blank">KeyboardEvent</a>
     * object with properties that describe the key press event:
     *
     * ```js
     * function keyReleased(event) {
     *   // Code to run that uses the event.
     *   console.log(event);
     * }
     * ```
     *
     * Note: Use the <a href="#/p5/keyPressed">keyPressed()</a> function and
     * <a href="#/p5/keyCode">keyCode</a> system variable to respond to modifier
     * keys such as `ALT`.
     *
     * Browsers may have default behaviors attached to various key events. To
     * prevent any default behavior for this event, add `return false;` to the end
     * of the function.
     *
     * @method keyTyped
     * @param  {KeyboardEvent} [event] optional `KeyboardEvent` callback argument.
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     * // Note: Pressing special keys such as SPACE have no effect.
     *
     * let value = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a white square at its center. The inner square changes color when the user presses a key.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the square.
     *   fill(value);
     *
     *   // Draw the square.
     *   square(25, 25, 50);
     * }
     *
     * // Toggle the square's color when the user types a printable key.
     * function keyTyped() {
     *   if (value === 0) {
     *     value = 255;
     *   } else {
     *     value = 0;
     *   }
     *   // Uncomment to prevent any default behavior.
     *   // return false;
     * }
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     *
     * let value = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a white square at its center. The inner square turns black when the user types the "b" key. It turns white when the user types the "a" key.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the square.
     *   fill(value);
     *
     *   // Draw the square.
     *   square(25, 25, 50);
     * }
     *
     * // Reassign value when the user types the 'a' or 'b' key.
     * function keyTyped() {
     *   if (key === 'a') {
     *     value = 255;
     *   } else if (key === 'b') {
     *     value = 0;
     *   }
     *   // Uncomment to prevent any default behavior.
     *   // return false;
     * }
     */
    fn._onkeypress = function(e) {
      if (e.which === this._lastKeyCodeTyped && e.repeat) {
        // prevent multiple firings
        return;
      }
      this._lastKeyCodeTyped = e.which; // track last keyCode
      this.key = e.key || String.fromCharCode(e.which) || e.which;

      if (typeof this._customActions.keyTyped === 'function') {
        const executeDefault = this._customActions.keyTyped(e);
        if (executeDefault === false) {
          e.preventDefault();
        }
      }
    };

    /**
     * The onblur function is called when the user is no longer focused
     * on the p5 element. Because the keyup events will not fire if the user is
     * not focused on the element we must assume all keys currently down have
     * been released.
     */
    fn._onblur = function(e) {
      this._downKeys = {};
    };

    /**
     * Returns `true` if the key it’s checking is pressed and `false` if not.
     *
     * `keyIsDown()` is helpful when checking for multiple different key presses.
     * For example, `keyIsDown()` can be used to check if both `LEFT_ARROW` and
     * `UP_ARROW` are pressed:
     *
     * ```js
     * if (keyIsDown(LEFT_ARROW) && keyIsDown(UP_ARROW)) {
     *   // Move diagonally.
     * }
     * ```
     *
     * `keyIsDown()` can check for key presses using strings based on
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key" target="_blank">KeyboardEvent.key</a>
     * or <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code" target="_blank">KeyboardEvent.code</a> values,
     * such as `keyIsDown('x')` or `keyIsDown('ArrowLeft')`.
     *
     * Note: In p5.js 2.0 and newer, numeric keycodes (such as 88 for 'X') are no longer supported.
     * This is a breaking change from previous versions.
     *
     * You can still use the p5 constants like `LEFT_ARROW` which now map to string values
     * internally rather than numeric codes.
     *
     * @method keyIsDown
     * @param {Number|String}   code key to check.
     * @return {Boolean}        whether the key is down or not.
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     *
     * let x = 50;
     * let y = 50;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   describe(
     *     'A gray square with a black circle at its center. The circle moves when the user presses an arrow key. It leaves a trail as it moves.'
     *   );
     * }
     *
     * function draw() {
     *   // Update x and y if an arrow key is pressed.
     *   if (keyIsDown(LEFT_ARROW) === true) {
     *     x -= 1;
     *   }
     *
     *   if (keyIsDown(RIGHT_ARROW) === true) {
     *     x += 1;
     *   }
     *
     *   if (keyIsDown(UP_ARROW) === true) {
     *     y -= 1;
     *   }
     *
     *   if (keyIsDown(DOWN_ARROW) === true) {
     *     y += 1;
     *   }
     *
     *   // Style the circle.
     *   fill(0);
     *
     *   // Draw the circle.
     *   circle(x, y, 5);
     * }
     *
     * @example
     * // Click on the canvas to begin detecting key presses.
     *
     * let x = 50;
     * let y = 50;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   describe(
     *     'A gray square with a black circle at its center. The circle moves when the user presses an arrow key. It leaves a trail as it moves.'
     *   );
     * }
     *
     * function draw() {
     *   // Update x and y if an arrow key is pressed.
     *   if (keyIsDown('ArrowLeft') === true) {
     *     x -= 1;
     *   }
     *
     *   if (keyIsDown('ArrowRight') === true) {
     *     x += 1;
     *   }
     *
     *   if (keyIsDown('ArrowUp') === true) {
     *     y -= 1;
     *   }
     *
     *   if (keyIsDown('ArrowDown') === true) {
     *     y += 1;
     *   }
     *
     *   // Style the circle.
     *   fill(0);
     *
     *   // Draw the circle.
     *   circle(x, y, 5);
     * }
     */
    fn.keyIsDown = function(input) {
      if (isCode(input)) {
        return this._downKeyCodes[input] || this._downKeys[input] || false;
      } else {
        return this._downKeys[input] || this._downKeyCodes[input] || false;
      }
    };

    /**
     * The _areDownKeys function returns a boolean true if any keys pressed
     * and a false if no keys are currently pressed.
     *
     * Helps avoid instances where multiple keys are pressed simultaneously and
     * releasing a single key will then switch the
     * keyIsPressed property to true.
     * @private
     */
    fn._areDownKeys = function() {
      for (const key in this._downKeys) {
        if (this._downKeys.hasOwnProperty(key) && this._downKeys[key] === true) {
          return true;
        }
      }
      return false;
    };
  }

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

  /**
   * @module Events
   * @submodule Pointer
   * @for p5
   * @requires core
   * @requires constants
   */

  function pointer(p5, fn, lifecycles){
    lifecycles.presetup = function(){
      const events = [
        'pointerdown',
        'pointerup',
        'pointermove',
        'dragend',
        'dragover',
        'click',
        'dblclick',
        'wheel'
      ];
      for(const event of events){
        window.addEventListener(event, this[`_on${event}`].bind(this), {
          passive: false,
          signal: this._removeSignal
        });
      }
    };

    /**
     * A `Number` system variable that tracks the mouse's horizontal movement.
     *
     * `movedX` tracks how many pixels the mouse moves left or right between
     * frames. `movedX` will have a negative value if the mouse moves left between
     * frames and a positive value if it moves right. `movedX` can be calculated
     * as `mouseX - pmouseX`.
     *
     * Note: `movedX` continues updating even when
     * <a href="#/p5/requestPointerLock">requestPointerLock()</a> is active.
     *
     * @property {Number} movedX
     * @readOnly
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square. The text ">>" appears when the user moves the mouse to the right. The text "<<" appears when the user moves the mouse to the left.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display >> when movedX is positive and
     *   // << when it's negative.
     *   if (movedX > 0) {
     *     text('>>', 50, 50);
     *   } else if (movedX < 0) {
     *     text('<<', 50, 50);
     *   }
     * }
     */
    fn.movedX = 0;

    /**
     * A `Number` system variable that tracks the mouse's vertical movement.
     *
     * `movedY` tracks how many pixels the mouse moves up or down between
     * frames. `movedY` will have a negative value if the mouse moves up between
     * frames and a positive value if it moves down. `movedY` can be calculated
     * as `mouseY - pmouseY`.
     *
     * Note: `movedY` continues updating even when
     * <a href="#/p5/requestPointerLock">requestPointerLock()</a> is active.
     *
     * @property {Number} movedY
     * @readOnly
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square. The text "▲" appears when the user moves the mouse upward. The text "▼" appears when the user moves the mouse downward.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display ▼ when movedY is positive and
     *   // ▲ when it's negative.
     *   if (movedY > 0) {
     *     text('▼', 50, 50);
     *   } else if (movedY < 0) {
     *     text('▲', 50, 50);
     *   }
     * }
     */
    fn.movedY = 0;

    /*
     * This is a flag which is false until the first time
     * we receive a mouse event. The pmouseX and pmouseY
     * values will match the mouseX and mouseY values until
     * this interaction takes place.
     */
    fn._hasMouseInteracted = false;

    /**
     * A `Number` system variable that tracks the mouse's horizontal position.
     *
     * `mouseX` keeps track of the mouse's position relative to the
     * top-left corner of the canvas. For example, if the mouse is 50 pixels from
     * the left edge of the canvas, then `mouseX` will be 50.
     *
     * If touch is used instead of the mouse, then `mouseX` will hold the
     * x-coordinate of the most recent touch point.
     *
     * @property {Number} mouseX
     * @readOnly
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe("A vertical black line moves left and right following the mouse's x-position.");
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw a vertical line that follows the mouse's x-coordinate.
     *   line(mouseX, 0, mouseX, 100);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe("A gray square. The mouse's x- and y-coordinates are displayed as the user moves the mouse.");
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the mouse's coordinates.
     *   text(`x: ${int(mouseX)} y: ${int(mouseY)}`, 50, 50);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe("A vertical black line moves left and right following the mouse's x-position.");
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Adjust coordinates for WebGL mode.
     *   // The origin (0, 0) is at the center of the canvas.
     *   let mx = mouseX - 50;
     *
     *   // Draw the line.
     *   line(mx, -50, mx, 50);
     * }
     *
     * @example
     * let font;
     *
     * async function setup() {
     *   // Load a font for WebGL mode.
     *   font = await loadFont('assets/inconsolata.otf');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     "A gray square. The mouse's x- and y-coordinates are displayed as the user moves the mouse."
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Display the mouse's coordinates.
     *   text(`x: ${int(mouseX)} y: ${int(mouseY)}`, 0, 0);
     * }
     */
    fn.mouseX = 0;

    /**
     * A `Number` system variable that tracks the mouse's vertical position.
     *
     * `mouseY` keeps track of the mouse's position relative to the
     * top-left corner of the canvas. For example, if the mouse is 50 pixels from
     * the top edge of the canvas, then `mouseY` will be 50.
     *
     * If touch is used instead of the mouse, then `mouseY` will hold the
     * y-coordinate of the most recent touch point.
     *
     * @property {Number} mouseY
     * @readOnly
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe("A horizontal black line moves up and down following the mouse's y-position.");
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw a horizontal line that follows the mouse's y-coordinate.
     *   line(0, mouseY, 100, mouseY);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe("A gray square. The mouse's x- and y-coordinates are displayed as the user moves the mouse.");
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the mouse's coordinates.
     *   text(`x: ${int(mouseX)} y: ${int(mouseY)}`, 50, 50);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe("A horizontal black line moves up and down following the mouse's y-position.");
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Adjust coordinates for WebGL mode.
     *   // The origin (0, 0) is at the center of the canvas.
     *   let my = mouseY - 50;
     *
     *   // Draw the line.
     *   line(-50, my, 50, my);
     * }
     *
     * @example
     * let font;
     *
     * async function setup() {
     *   // Load a font for WebGL mode.
     *   font = await loadFont('assets/inconsolata.otf');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe(
     *     "A gray square. The mouse's x- and y-coordinates are displayed as the user moves the mouse."
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *   textFont(font);
     *   fill(0);
     *
     *   // Display the mouse's coordinates.
     *   text(`x: ${int(mouseX)} y: ${int(mouseY)}`, 0, 0);
     * }
     */
    fn.mouseY = 0;

    /**
     * A `Number` system variable that tracks the mouse's previous horizontal
     * position.
     *
     * `pmouseX` keeps track of the mouse's position relative to the
     * top-left corner of the canvas. Its value is
     * <a href="#/p5/mouseX">mouseX</a> from the previous frame. For example, if
     * the mouse was 50 pixels from the left edge of the canvas during the last
     * frame, then `pmouseX` will be 50.
     *
     * If touch is used instead of the mouse, then `pmouseX` will hold the
     * x-coordinate of the last touch point.
     *
     * Note: `pmouseX` is reset to the current <a href="#/p5/mouseX">mouseX</a>
     * value at the start of each touch event.
     *
     * @property {Number} pmouseX
     * @readOnly
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Slow the frame rate.
     *   frameRate(10);
     *
     *   describe('A line follows the mouse as it moves. The line grows longer with faster movements.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   line(pmouseX, pmouseY, mouseX, mouseY);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A line follows the mouse as it moves. The line grows longer with faster movements.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Adjust coordinates for WebGL mode.
     *   // The origin (0, 0) is at the center of the canvas.
     *   let pmx = pmouseX - 50;
     *   let pmy = pmouseY - 50;
     *   let mx = mouseX - 50;
     *   let my = mouseY - 50;
     *
     *   // Draw the line.
     *   line(pmx, pmy, mx, my);
     * }
     */
    fn.pmouseX = 0;

    /**
     * A `Number` system variable that tracks the mouse's previous vertical
     * position.
     *
     * `pmouseY` keeps track of the mouse's position relative to the
     * top-left corner of the canvas. Its value is
     * <a href="#/p5/mouseY">mouseY</a> from the previous frame. For example, if
     * the mouse was 50 pixels from the top edge of the canvas during the last
     * frame, then `pmouseY` will be 50.
     *
     * If touch is used instead of the mouse, then `pmouseY` will hold the
     * y-coordinate of the last touch point.
     *
     * Note: `pmouseY` is reset to the current <a href="#/p5/mouseY">mouseY</a>
     * value at the start of each touch event.
     *
     * @property {Number} pmouseY
     * @readOnly
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Slow the frame rate.
     *   frameRate(10);
     *
     *   describe('A line follows the mouse as it moves. The line grows longer with faster movements.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   line(pmouseX, pmouseY, mouseX, mouseY);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A line follows the mouse as it moves. The line grows longer with faster movements.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Adjust coordinates for WebGL mode.
     *   // The origin (0, 0) is at the center of the canvas.
     *   let pmx = pmouseX - 50;
     *   let pmy = pmouseY - 50;
     *   let mx = mouseX - 50;
     *   let my = mouseY - 50;
     *
     *   // Draw the line.
     *   line(pmx, pmy, mx, my);
     * }
     */
    fn.pmouseY = 0;

    /**
     * A `Number` variable that tracks the mouse's horizontal position within the
     * browser.
     *
     * `winMouseX` keeps track of the mouse's position relative to the top-left
     * corner of the browser window. For example, if the mouse is 50 pixels from
     * the left edge of the browser, then `winMouseX` will be 50.
     *
     * On a touchscreen device, `winMouseX` will hold the x-coordinate of the most
     * recent touch point.
     *
     * Note: Use <a href="#/p5/mouseX">mouseX</a> to track the mouse’s
     * x-coordinate within the canvas.
     *
     * @property {Number} winMouseX
     * @readOnly
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe("A gray square. The mouse's x- and y-coordinates are displayed as the user moves the mouse.");
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the mouse's coordinates within the browser window.
     *   text(`x: ${int(winMouseX)} y: ${int(winMouseY)}`, 50, 50);
     * }
     */
    fn.winMouseX = 0;

    /**
     * A `Number` variable that tracks the mouse's vertical position within the
     * browser.
     *
     * `winMouseY` keeps track of the mouse's position relative to the top-left
     * corner of the browser window. For example, if the mouse is 50 pixels from
     * the top edge of the browser, then `winMouseY` will be 50.
     *
     * On a touchscreen device, `winMouseY` will hold the y-coordinate of the most
     * recent touch point.
     *
     * Note: Use <a href="#/p5/mouseY">mouseY</a> to track the mouse’s
     * y-coordinate within the canvas.
     *
     * @property {Number} winMouseY
     * @readOnly
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe("A gray square. The mouse's x- and y-coordinates are displayed as the user moves the mouse.");
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the mouse's coordinates within the browser window.
     *   text(`x: ${int(winMouseX)} y: ${int(winMouseY)}`, 50, 50);
     * }
     */
    fn.winMouseY = 0;

    /**
     * A `Number` variable that tracks the mouse's previous horizontal position
     * within the browser.
     *
     * `pwinMouseX` keeps track of the mouse's position relative to the top-left
     * corner of the browser window. Its value is
     * <a href="#/p5/winMouseX">winMouseX</a> from the previous frame. For
     * example, if the mouse was 50 pixels from
     * the left edge of the browser during the last frame, then `pwinMouseX` will
     * be 50.
     *
     * On a touchscreen device, `pwinMouseX` will hold the x-coordinate of the most
     * recent touch point. `pwinMouseX` is reset to the current
     * <a href="#/p5/winMouseX">winMouseX</a> value at the start of each touch
     * event.
     *
     * Note: Use <a href="#/p5/pmouseX">pmouseX</a> to track the mouse’s previous
     * x-coordinate within the canvas.
     *
     * @property {Number} pwinMouseX
     * @readOnly
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Slow the frame rate.
     *   frameRate(10);
     *
     *   describe('A gray square. A white circle at its center grows larger when the mouse moves horizontally.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the circle's diameter.
     *   let d = winMouseX - pwinMouseX;
     *
     *   // Draw the circle.
     *   circle(50, 50, d);
     * }
     *
     * @example
     * function setup() {
     *   // Create the canvas and set its position.
     *   let cnv = createCanvas(100, 100);
     *   cnv.position(20, 20);
     *
     *   describe('A gray square with a number at its center. The number changes as the user moves the mouse vertically.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display pwinMouseX.
     *   text(pwinMouseX, 50, 50);
     * }
     */
    fn.pwinMouseX = 0;

    /**
     * A `Number` variable that tracks the mouse's previous vertical position
     * within the browser.
     *
     * `pwinMouseY` keeps track of the mouse's position relative to the top-left
     * corner of the browser window. Its value is
     * <a href="#/p5/winMouseY">winMouseY</a> from the previous frame. For
     * example, if the mouse was 50 pixels from
     * the top edge of the browser during the last frame, then `pwinMouseY` will
     * be 50.
     *
     * On a touchscreen device, `pwinMouseY` will hold the y-coordinate of the most
     * recent touch point. `pwinMouseY` is reset to the current
     * <a href="#/p5/winMouseY">winMouseY</a> value at the start of each touch
     * event.
     *
     * Note: Use <a href="#/p5/pmouseY">pmouseY</a> to track the mouse’s previous
     * y-coordinate within the canvas.
     *
     * @property {Number} pwinMouseY
     * @readOnly
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Slow the frame rate.
     *   frameRate(10);
     *
     *   describe('A gray square. A white circle at its center grows larger when the mouse moves vertically.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the circle's diameter.
     *   let d = winMouseY - pwinMouseY;
     *
     *   // Draw the circle.
     *   circle(50, 50, d);
     * }
     *
     * @example
     * function setup() {
     *   // Create the canvas and set its position.
     *   let cnv = createCanvas(100, 100);
     *   cnv.position(20, 20);
     *
     *   describe('A gray square with a number at its center. The number changes as the user moves the mouse vertically.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display pwinMouseY.
     *   text(pwinMouseY, 50, 50);
     * }
     */
    fn.pwinMouseY = 0;

    /**
     * An object that tracks the current state of mouse buttons, showing which
     * buttons are pressed at any given moment.
     *
     * The `mouseButton` object has three properties:
     * - `left`: A boolean indicating whether the left mouse button is pressed.
     * - `right`: A boolean indicating whether the right mouse button is pressed.
     * - `center`: A boolean indicating whether the middle mouse button (scroll wheel button) is pressed.
     *
     * Note: Different browsers may track `mouseButton` differently. See
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons" target="_blank">MDN</a>
     * for more information.
     *
     * @property {Object} mouseButton
     * @property {boolean} mouseButton.left - Whether the left mouse button is pressed.
     * @property {boolean} mouseButton.right - Whether the right mouse button is pressed.
     * @property {boolean} mouseButton.center - Whether the middle mouse button is pressed.
     * @readOnly
     *
     * @example
     * function setup() {
     *   createCanvas(200, 200);
     *
     *   describe(
     *     'A gray square with black text at its center. The text changes from 0 to either "left" or "right" when the user clicks a mouse button.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(16);
     *
     *   // Display the mouse button.
     *   text(`Left: ${mouseButton.left}`, width / 2, height / 2 - 20);
     *   text(`Right: ${mouseButton.right}`, width / 2, height / 2);
     *   text(`Center: ${mouseButton.center}`, width / 2, height / 2 + 20);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     "A gray square. Different shapes appear at its center depending on the mouse button that's clicked."
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   if (mouseIsPressed === true) {
     *     if (mouseButton.left) {
     *       circle(50, 50, 50);
     *     }
     *     if (mouseButton.right) {
     *       square(25, 25, 50);
     *     }
     *     if (mouseButton.center) {
     *       triangle(23, 75, 50, 20, 78, 75);
     *     }
     *   }
     * }
     */
    fn.mouseButton = {
      left: false,
      right: false,
      center: false
    };

    /**
     * An `Array` of all the current touch points on a touchscreen device.
     *
     * The `touches` array is empty by default. When the user touches their
     * screen, a new touch point is tracked and added to the array. Touch points
     * are `Objects` with the following properties:
     *
     * ```js
     * // Iterate over the touches array.
     * for (let touch of touches) {
     *   // x-coordinate relative to the top-left
     *   // corner of the canvas.
     *   console.log(touch.x);
     *
     *   // y-coordinate relative to the top-left
     *   // corner of the canvas.
     *   console.log(touch.y);
     *
     *   // x-coordinate relative to the top-left
     *   // corner of the browser.
     *   console.log(touch.winX);
     *
     *   // y-coordinate relative to the top-left
     *   // corner of the browser.
     *   console.log(touch.winY);
     *
     *   // ID number
     *   console.log(touch.id);
     * }
     * ```
     *
     * @property {Object[]} touches
     * @readOnly
     *
     * @example
     * // On a touchscreen device, touch the canvas using one or more fingers
     * // at the same time.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square. White circles appear where the user touches the square.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw a circle at each touch point.
     *   for (let touch of touches) {
     *     circle(touch.x, touch.y, 40);
     *   }
     * }
     *
     * @example
     * // On a touchscreen device, touch the canvas using one or more fingers
     * // at the same time.
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square. Labels appear where the user touches the square, displaying the coordinates.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw a label above each touch point.
     *   for (let touch of touches) {
     *     text(`${touch.x}, ${touch.y}`, touch.x, touch.y - 40);
     *   }
     * }
     */
    fn.touches = [];
    fn._activePointers = new Map();

    /**
     * A `Boolean` system variable that's `true` if the mouse is pressed and
     * `false` if not.
     *
     * @property {Boolean} mouseIsPressed
     * @readOnly
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with the word "false" at its center. The word changes to "true" when the user presses a mouse button.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the mouseIsPressed variable.
     *   text(mouseIsPressed, 25, 50);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a white square at its center. The inner square turns black when the user presses the mouse.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the square.
     *   if (mouseIsPressed === true) {
     *     fill(0);
     *   } else {
     *     fill(255);
     *   }
     *
     *   // Draw the square.
     *   square(25, 25, 50);
     * }
     */
    fn.mouseIsPressed = false;

    fn._updatePointerCoords = function (e) {
      if (this._curElement !== null) {
        const canvas = this._curElement.elt;
        const sx = canvas.scrollWidth / this.width || 1;
        const sy = canvas.scrollHeight / this.height || 1;

        if (e.pointerType === 'touch') {
          const touches = [];
          for (const touch of this._activePointers.values()) {
            touches.push(getTouchInfo(canvas, sx, sy, touch));
          }
          this.touches = touches;
        }

        const mousePos = getMouseInfo(canvas, sx, sy, e);
        this.movedX = e.movementX || 0;
        this.movedY = e.movementY || 0;
        this.mouseX = mousePos.x;
        this.mouseY = mousePos.y;
        this.winMouseX = mousePos.winX;
        this.winMouseY = mousePos.winY;

        if (!this._hasMouseInteracted) {
          this._updateMouseCoords();
          this._hasMouseInteracted = true;
        }
      }
    };

    fn._updateMouseCoords = function() {
      this.pmouseX = this.mouseX;
      this.pmouseY = this.mouseY;
      this.pwinMouseX = this.winMouseX;
      this.pwinMouseY = this.winMouseY;
      this._pmouseWheelDeltaY = this._mouseWheelDeltaY;
    };

    function getMouseInfo(canvas, sx, sy, evt) {
      const rect = canvas.getBoundingClientRect();
      return {
        x: (evt.clientX - rect.left) / sx,
        y: (evt.clientY - rect.top) / sy,
        winX: evt.clientX,
        winY: evt.clientY
      };
    }

    function getTouchInfo(canvas, sx, sy, touch) {
      const rect = canvas.getBoundingClientRect();
      return {
        x: (touch.clientX - rect.left) / sx,
        y: (touch.clientY - rect.top) / sy,
        winX: touch.clientX,
        winY: touch.clientY,
        id: touch.pointerId
      };
    }

    fn._setMouseButton = function(e) {
      // Check all active touches to determine button states
      this.mouseButton.left = Array.from(this._activePointers.values())
        .some(touch =>
          (touch.buttons & 1) !== 0
        );
      this.mouseButton.center = Array.from(this._activePointers.values())
        .some(touch =>
          (touch.buttons & 4) !== 0
        );
      this.mouseButton.right = Array.from(this._activePointers.values())
        .some(touch =>
          (touch.buttons & 2) !== 0
        );
    };

    /**
     * A function that's called when the mouse moves.
     *
     * Declaring the function `mouseMoved()` sets a code block to run
     * automatically when the user moves the mouse without clicking any mouse
     * buttons:
     *
     * ```js
     * function mouseMoved() {
     *   // Code to run.
     * }
     * ```
     *
     * The mouse system variables, such as <a href="#/p5/mouseX">mouseX</a> and
     * <a href="#/p5/mouseY">mouseY</a>, will be updated with their most recent
     * value when `mouseMoved()` is called by p5.js:
     *
     * ```js
     * function mouseMoved() {
     *   if (mouseX < 50) {
     *     // Code to run if the mouse is on the left.
     *   }
     *
     *   if (mouseY > 50) {
     *     // Code to run if the mouse is near the bottom.
     *   }
     * }
     * ```
     *
     * The parameter, `event`, is optional. `mouseMoved()` is always passed a
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a>
     * object with properties that describe the mouse move event:
     *
     * ```js
     * function mouseMoved(event) {
     *   // Code to run that uses the event.
     *   console.log(event);
     * }
     * ```
     *
     * Browsers may have default behaviors attached to various mouse events. For
     * example, some browsers highlight text when the user moves the mouse while
     * pressing a mouse button. To prevent any default behavior for this event,
     * add `return false;` to the end of the function.
     *
     * @method mouseMoved
     * @param  {MouseEvent} [event] optional `MouseEvent` argument.
     *
     * @example
     * let value = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a black square at its center. The inner square becomes lighter as the mouse moves.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the square.
     *   fill(value);
     *
     *   // Draw the square.
     *   square(25, 25, 50);
     * }
     *
     * function mouseMoved() {
     *   // Update the grayscale value.
     *   value += 5;
     *
     *   // Reset the grayscale value.
     *   if (value > 255) {
     *     value = 0;
     *   }
     *   // Uncomment to prevent any default behavior.
     *   // return false;
     * }
     */

    /**
     * A function that's called when the mouse moves while a button is pressed.
     *
     * Declaring the function `mouseDragged()` sets a code block to run
     * automatically when the user clicks and drags the mouse:
     *
     * ```js
     * function mouseDragged() {
     *   // Code to run.
     * }
     * ```
     *
     * The mouse system variables, such as <a href="#/p5/mouseX">mouseX</a> and
     * <a href="#/p5/mouseY">mouseY</a>, will be updated with their most recent
     * value when `mouseDragged()` is called by p5.js:
     *
     * ```js
     * function mouseDragged() {
     *   if (mouseX < 50) {
     *     // Code to run if the mouse is on the left.
     *   }
     *
     *   if (mouseY > 50) {
     *     // Code to run if the mouse is near the bottom.
     *   }
     * }
     * ```
     *
     * The parameter, `event`, is optional. `mouseDragged()` is always passed a
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a>
     * object with properties that describe the mouse drag event:
     *
     * ```js
     * function mouseDragged(event) {
     *   // Code to run that uses the event.
     *   console.log(event);
     * }
     * ```
     *
     * On touchscreen devices, `mouseDragged()` will run when a user moves a touch
     * point.
     *
     * Browsers may have default behaviors attached to various mouse events. For
     * example, some browsers highlight text when the user moves the mouse while
     * pressing a mouse button. To prevent any default behavior for this event,
     * add `return false;` to the end of the function.
     *
     * @method mouseDragged
     * @param  {MouseEvent} [event] optional `MouseEvent` argument.
     *
     * @example
     * let value = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a black square at its center. The inner square becomes lighter as the user drags the mouse.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the square.
     *   fill(value);
     *
     *   // Draw the square.
     *   square(25, 25, 50);
     * }
     *
     * function mouseDragged() {
     *   // Update the grayscale value.
     *   value += 5;
     *
     *   // Reset the grayscale value.
     *   if (value > 255) {
     *     value = 0;
     *   }
     *   // Uncomment to prevent any default behavior.
     *   // return false;
     * }
     */
    fn._onpointermove = function(e) {
      let executeDefault;
      this._updatePointerCoords(e);
      this._activePointers.set(e.pointerId, e);
      this._setMouseButton(e);

      if (
        !this.mouseIsPressed &&
        typeof this._customActions.mouseMoved === 'function'
      ) {
        executeDefault = this._customActions.mouseMoved(e);
        if (executeDefault === false) {
          e.preventDefault();
        }
      } else if (
        this.mouseIsPressed &&
        typeof this._customActions.mouseDragged === 'function'
      ) {
        executeDefault = this._customActions.mouseDragged(e);
        if (executeDefault === false) {
          e.preventDefault();
        }
      }
    };

    /**
     * A function that's called once when a mouse button is pressed.
     *
     * Declaring the function `mousePressed()` sets a code block to run
     * automatically when the user presses a mouse button:
     *
     * ```js
     * function mousePressed() {
     *   // Code to run.
     * }
     * ```
     *
     * The mouse system variables, such as <a href="#/p5/mouseX">mouseX</a> and
     * <a href="#/p5/mouseY">mouseY</a>, will be updated with their most recent
     * value when `mousePressed()` is called by p5.js:
     *
     * ```js
     * function mousePressed() {
     *   if (mouseX < 50) {
     *     // Code to run if the mouse is on the left.
     *   }
     *
     *   if (mouseY > 50) {
     *     // Code to run if the mouse is near the bottom.
     *   }
     * }
     * ```
     *
     * The parameter, `event`, is optional. `mousePressed()` is always passed a
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a>
     * object with properties that describe the mouse press event:
     *
     * ```js
     * function mousePressed(event) {
     *   // Code to run that uses the event.
     *   console.log(event);
     * }
     * ```
     *
     * On touchscreen devices, `mousePressed()` will run when a user’s touch
     * begins.
     *
     * Browsers may have default behaviors attached to various mouse events. For
     * example, some browsers highlight text when the user moves the mouse while
     * pressing a mouse button. To prevent any default behavior for this event,
     * add `return false;` to the end of the function.
     *
     * Note: `mousePressed()`, <a href="#/p5/mouseReleased">mouseReleased()</a>,
     * and <a href="#/p5/mouseClicked">mouseClicked()</a> are all related.
     * `mousePressed()` runs as soon as the user clicks the mouse.
     * <a href="#/p5/mouseReleased">mouseReleased()</a> runs as soon as the user
     * releases the mouse click. <a href="#/p5/mouseClicked">mouseClicked()</a>
     * runs immediately after <a href="#/p5/mouseReleased">mouseReleased()</a>.
     *
     * @method mousePressed
     * @param  {MouseEvent} [event] optional `MouseEvent` argument.
     *
     * @example
     * let value = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a black square at its center. The inner square becomes lighter when the user presses a mouse button.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the square.
     *   fill(value);
     *
     *   // Draw the square.
     *   square(25, 25, 50);
     * }
     *
     * function mousePressed() {
     *   // Update the grayscale value.
     *   value += 5;
     *
     *   // Reset the grayscale value.
     *   if (value > 255) {
     *     value = 0;
     *   }
     *   // Uncomment to prevent any default behavior.
     *   // return false;
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Style the circle.
     *   fill('orange');
     *   stroke('royalblue');
     *   strokeWeight(10);
     *
     *   describe(
     *     'An orange circle with a thick, blue border drawn on a gray background. When the user presses and holds the mouse, the border becomes thin and pink. When the user releases the mouse, the border becomes thicker and changes color to blue.'
     *   );
     * }
     *
     * function draw() {
     *   background(220);
     *
     *   // Draw the circle.
     *   circle(50, 50, 20);
     * }
     *
     * // Set the stroke color and weight as soon as the user clicks.
     * function mousePressed() {
     *   stroke('deeppink');
     *   strokeWeight(3);
     * }
     *
     * // Set the stroke and fill colors as soon as the user releases
     * // the mouse.
     * function mouseReleased() {
     *   stroke('royalblue');
     *
     *   // This is never visible because fill() is called
     *   // in mouseClicked() which runs immediately after
     *   // mouseReleased();
     *   fill('limegreen');
     * }
     *
     * // Set the fill color and stroke weight after
     * // mousePressed() and mouseReleased() are called.
     * function mouseClicked() {
     *   fill('orange');
     *   strokeWeight(10);
     * }
     */
    fn._onpointerdown = function(e) {
      let executeDefault;
      this.mouseIsPressed = true;

      this._activePointers.set(e.pointerId, e);
      this._setMouseButton(e);
      this._updatePointerCoords(e);

      if (typeof this._customActions.mousePressed === 'function') {
        executeDefault = this._customActions.mousePressed(e);
        if (executeDefault === false) {
          e.preventDefault();
        }
      }
    };

    /**
     * A function that's called once when a mouse button is released.
     *
     * Declaring the function `mouseReleased()` sets a code block to run
     * automatically when the user releases a mouse button after having pressed
     * it:
     *
     * ```js
     * function mouseReleased() {
     *   // Code to run.
     * }
     * ```
     *
     * The mouse system variables, such as <a href="#/p5/mouseX">mouseX</a> and
     * <a href="#/p5/mouseY">mouseY</a>, will be updated with their most recent
     * value when `mouseReleased()` is called by p5.js:
     *
     * ```js
     * function mouseReleased() {
     *   if (mouseX < 50) {
     *     // Code to run if the mouse is on the left.
     *   }
     *
     *   if (mouseY > 50) {
     *     // Code to run if the mouse is near the bottom.
     *   }
     * }
     * ```
     *
     * The parameter, `event`, is optional. `mouseReleased()` is always passed a
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a>
     * object with properties that describe the mouse release event:
     *
     * ```js
     * function mouseReleased(event) {
     *   // Code to run that uses the event.
     *   console.log(event);
     * }
     * ```
     *
     * On touchscreen devices, `mouseReleased()` will run when a user’s touch
     * ends.
     *
     * Browsers may have default behaviors attached to various mouse events. For
     * example, some browsers highlight text when the user moves the mouse while
     * pressing a mouse button. To prevent any default behavior for this event,
     * add `return false;` to the end of the function.
     *
     * Note: <a href="#/p5/mousePressed">mousePressed()</a>, `mouseReleased()`,
     * and <a href="#/p5/mouseClicked">mouseClicked()</a> are all related.
     * <a href="#/p5/mousePressed">mousePressed()</a> runs as soon as the user
     * clicks the mouse. `mouseReleased()` runs as soon as the user releases the
     * mouse click. <a href="#/p5/mouseClicked">mouseClicked()</a> runs
     * immediately after `mouseReleased()`.
     *
     * @method mouseReleased
     * @param  {MouseEvent} [event] optional `MouseEvent` argument.
     *
     * @example
     * let value = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a black square at its center. The inner square becomes lighter when the user presses and releases a mouse button.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the square.
     *   fill(value);
     *
     *   // Draw the square.
     *   square(25, 25, 50);
     * }
     *
     * function mouseReleased() {
     *   // Update the grayscale value.
     *   value += 5;
     *
     *   // Reset the grayscale value.
     *   if (value > 255) {
     *     value = 0;
     *   }
     *   // Uncomment to prevent any default behavior.
     *   // return false;
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Style the circle.
     *   fill('orange');
     *   stroke('royalblue');
     *   strokeWeight(10);
     *
     *   describe(
     *     'An orange circle with a thick, blue border drawn on a gray background. When the user presses and holds the mouse, the border becomes thin and pink. When the user releases the mouse, the border becomes thicker and changes color to blue.'
     *   );
     * }
     *
     * function draw() {
     *   background(220);
     *
     *   // Draw the circle.
     *   circle(50, 50, 20);
     * }
     *
     * // Set the stroke color and weight as soon as the user clicks.
     * function mousePressed() {
     *   stroke('deeppink');
     *   strokeWeight(3);
     * }
     *
     * // Set the stroke and fill colors as soon as the user releases
     * // the mouse.
     * function mouseReleased() {
     *   stroke('royalblue');
     *
     *   // This is never visible because fill() is called
     *   // in mouseClicked() which runs immediately after
     *   // mouseReleased();
     *   fill('limegreen');
     * }
     *
     * // Set the fill color and stroke weight after
     * // mousePressed() and mouseReleased() are called.
     * function mouseClicked() {
     *   fill('orange');
     *   strokeWeight(10);
     * }
     */
    fn._onpointerup = function(e) {
      let executeDefault;
      this.mouseIsPressed = false;

      this._activePointers.delete(e.pointerId);
      this._setMouseButton(e);

      this._updatePointerCoords(e);

      if (typeof this._customActions.mouseReleased === 'function') {
        executeDefault = this._customActions.mouseReleased(e);
        if (executeDefault === false) {
          e.preventDefault();
        }
      }
    };

    fn._ondragend = fn._onpointerup;
    fn._ondragover = fn._onpointermove;

    /**
     * A function that's called once after a mouse button is pressed and released.
     *
     * Declaring the function `mouseClicked()` sets a code block to run
     * automatically when the user releases a mouse button after having pressed
     * it:
     *
     * ```js
     * function mouseClicked() {
     *   // Code to run.
     * }
     * ```
     *
     * The mouse system variables, such as <a href="#/p5/mouseX">mouseX</a> and
     * <a href="#/p5/mouseY">mouseY</a>, will be updated with their most recent
     * value when `mouseClicked()` is called by p5.js:
     *
     * ```js
     * function mouseClicked() {
     *   if (mouseX < 50) {
     *     // Code to run if the mouse is on the left.
     *   }
     *
     *   if (mouseY > 50) {
     *     // Code to run if the mouse is near the bottom.
     *   }
     * }
     * ```
     *
     * The parameter, `event`, is optional. `mouseClicked()` is always passed a
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a>
     * object with properties that describe the mouse click event:
     *
     * ```js
     * function mouseClicked(event) {
     *   // Code to run that uses the event.
     *   console.log(event);
     * }
     * ```
     *
     * On touchscreen devices, `mouseClicked()` will run when a user’s touch
     * ends.
     *
     * Browsers may have default behaviors attached to various mouse events. For
     * example, some browsers highlight text when the user moves the mouse while
     * pressing a mouse button. To prevent any default behavior for this event,
     * add `return false;` to the end of the function.
     *
     * Note: <a href="#/p5/mousePressed">mousePressed()</a>,
     * <a href="#/p5/mouseReleased">mouseReleased()</a>,
     * and `mouseClicked()` are all related.
     * <a href="#/p5/mousePressed">mousePressed()</a> runs as soon as the user
     * clicks the mouse. <a href="#/p5/mouseReleased">mouseReleased()</a> runs as
     * soon as the user releases the mouse click. `mouseClicked()` runs
     * immediately after <a href="#/p5/mouseReleased">mouseReleased()</a>.
     *
     * @method mouseClicked
     * @param  {MouseEvent} [event] optional `MouseEvent` argument.
     *
     * @example
     * let value = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a black square at its center. The inner square changes color when the user presses and releases a mouse button.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the square.
     *   fill(value);
     *
     *   // Draw the square.
     *   square(25, 25, 50);
     * }
     *
     * // Toggle the square's color when the user clicks.
     * function mouseClicked() {
     *   if (value === 0) {
     *     value = 255;
     *   } else {
     *     value = 0;
     *   }
     *   // Uncomment to prevent any default behavior.
     *   // return false;
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Style the circle.
     *   fill('orange');
     *   stroke('royalblue');
     *   strokeWeight(10);
     *
     *   describe(
     *     'An orange circle with a thick, blue border drawn on a gray background. When the user presses and holds the mouse, the border becomes thin and pink. When the user releases the mouse, the border becomes thicker and changes color to blue.'
     *   );
     * }
     *
     * function draw() {
     *   background(220);
     *
     *   // Draw the circle.
     *   circle(50, 50, 20);
     * }
     *
     * // Set the stroke color and weight as soon as the user clicks.
     * function mousePressed() {
     *   stroke('deeppink');
     *   strokeWeight(3);
     * }
     *
     * // Set the stroke and fill colors as soon as the user releases
     * // the mouse.
     * function mouseReleased() {
     *   stroke('royalblue');
     *
     *   // This is never visible because fill() is called
     *   // in mouseClicked() which runs immediately after
     *   // mouseReleased();
     *   fill('limegreen');
     * }
     *
     * // Set the fill color and stroke weight after
     * // mousePressed() and mouseReleased() are called.
     * function mouseClicked() {
     *   fill('orange');
     *   strokeWeight(10);
     * }
     */
    fn._onclick = function(e) {
      if (typeof this._customActions.mouseClicked === 'function') {
        const executeDefault = this._customActions.mouseClicked(e);
        if (executeDefault === false) {
          e.preventDefault();
        }
      }
    };

    /**
     * A function that's called once when a mouse button is clicked twice quickly.
     *
     * Declaring the function `doubleClicked()` sets a code block to run
     * automatically when the user presses and releases the mouse button twice
     * quickly:
     *
     * ```js
     * function doubleClicked() {
     *   // Code to run.
     * }
     * ```
     *
     * The mouse system variables, such as <a href="#/p5/mouseX">mouseX</a> and
     * <a href="#/p5/mouseY">mouseY</a>, will be updated with their most recent
     * value when `doubleClicked()` is called by p5.js:
     *
     * ```js
     * function doubleClicked() {
     *   if (mouseX < 50) {
     *     // Code to run if the mouse is on the left.
     *   }
     *
     *   if (mouseY > 50) {
     *     // Code to run if the mouse is near the bottom.
     *   }
     * }
     * ```
     *
     * The parameter, `event`, is optional. `doubleClicked()` is always passed a
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a>
     * object with properties that describe the double-click event:
     *
     * ```js
     * function doubleClicked(event) {
     *   // Code to run that uses the event.
     *   console.log(event);
     * }
     * ```
     *
     * On touchscreen devices, code placed in `doubleClicked()` will run after two
     * touches that occur within a short time.
     *
     * Browsers may have default behaviors attached to various mouse events. For
     * example, some browsers highlight text when the user moves the mouse while
     * pressing a mouse button. To prevent any default behavior for this event,
     * add `return false;` to the end of the function.
     *
     * @method doubleClicked
     * @param  {MouseEvent} [event] optional `MouseEvent` argument.
     *
     * @example
     * let value = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a black square at its center. The inner square changes color when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the square.
     *   fill(value);
     *
     *   // Draw the square.
     *   square(25, 25, 50);
     * }
     *
     * // Toggle the square's color when the user double-clicks.
     * function doubleClicked() {
     *   if (value === 0) {
     *     value = 255;
     *   } else {
     *     value = 0;
     *   }
     *   // Uncomment to prevent any default behavior.
     *   // return false;
     * }
     *
     * @example
     * let value = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a black circle at its center. When the user double-clicks on the circle, it changes color to white.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the circle.
     *   fill(value);
     *
     *   // Draw the circle.
     *   circle(50, 50, 80);
     * }
     *
     * // Reassign value to 255 when the user double-clicks on the circle.
     * function doubleClicked() {
     *   if (dist(50, 50, mouseX, mouseY) < 40) {
     *     value = 255;
     *   }
     *   // Uncomment to prevent any default behavior.
     *   // return false;
     * }
     */

    fn._ondblclick = function(e) {
      if (typeof this._customActions.doubleClicked === 'function') {
        const executeDefault = this._customActions.doubleClicked(e);
        if (executeDefault === false) {
          e.preventDefault();
        }
      }
    };

    /**
     * For use with WebGL orbitControl.
     * @property {Number} _mouseWheelDeltaY
     * @readOnly
     * @private
     */
    fn._mouseWheelDeltaY = 0;

    /**
     * For use with WebGL orbitControl.
     * @property {Number} _pmouseWheelDeltaY
     * @readOnly
     * @private
     */
    fn._pmouseWheelDeltaY = 0;

    /**
     * A function that's called once when the mouse wheel moves.
     *
     * Declaring the function `mouseWheel()` sets a code block to run
     * automatically when the user scrolls with the mouse wheel:
     *
     * ```js
     * function mouseWheel() {
     *   // Code to run.
     * }
     * ```
     *
     * The mouse system variables, such as <a href="#/p5/mouseX">mouseX</a> and
     * <a href="#/p5/mouseY">mouseY</a>, will be updated with their most recent
     * value when `mouseWheel()` is called by p5.js:
     *
     * ```js
     * function mouseWheel() {
     *   if (mouseX < 50) {
     *     // Code to run if the mouse is on the left.
     *   }
     *
     *   if (mouseY > 50) {
     *     // Code to run if the mouse is near the bottom.
     *   }
     * }
     * ```
     *
     * The parameter, `event`, is optional. `mouseWheel()` is always passed a
     * <a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a>
     * object with properties that describe the mouse scroll event:
     *
     * ```js
     * function mouseWheel(event) {
     *   // Code to run that uses the event.
     *   console.log(event);
     * }
     * ```
     *
     * The `event` object has many properties including `delta`, a `Number`
     * containing the distance that the user scrolled. For example, `event.delta`
     * might have the value 5 when the user scrolls up. `event.delta` is positive
     * if the user scrolls up and negative if they scroll down. The signs are
     * opposite on macOS with "natural" scrolling enabled.
     *
     * Browsers may have default behaviors attached to various mouse events. For
     * example, some browsers highlight text when the user moves the mouse while
     * pressing a mouse button. To prevent any default behavior for this event,
     * add `return false;` to the end of the function.
     *
     * Note: On Safari, `mouseWheel()` may only work as expected if
     * `return false;` is added at the end of the function.
     *
     * @method mouseWheel
     * @param  {WheelEvent} [event] optional `WheelEvent` argument.
     *
     * @example
     * let circleSize = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square. A white circle at its center grows up when the user scrolls the mouse wheel.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw the circle
     *   circle(circleSize, 50, 50);
     * }
     *
     * // Increment circleSize when the user scrolls the mouse wheel.
     * function mouseWheel() {
     *   circleSize += 1;
     *   // Uncomment to prevent any default behavior.
     *   // return false;
     * }
     *
     * @example
     * let direction = '';
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square. An arrow at its center points up when the user scrolls up. The arrow points down when the user scrolls down.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Draw an arrow that points where
     *   // the mouse last scrolled.
     *   text(direction, 50, 50);
     * }
     *
     * // Change direction when the user scrolls the mouse wheel.
     * function mouseWheel(event) {
     *   if (event.delta > 0) {
     *     direction = '▲';
     *   } else {
     *     direction = '▼';
     *   }
     *   // Uncomment to prevent any default behavior.
     *   // return false;
     * }
     */
    fn._onwheel = function(e) {
      this._mouseWheelDeltaY = e.deltaY;
      if (typeof this._customActions.mouseWheel === 'function') {
        e.delta = e.deltaY;
        const executeDefault = this._customActions.mouseWheel(e);
        if (executeDefault === false) {
          e.preventDefault();
        }
      }
    };

    /**
     * Locks the mouse pointer to its current position and makes it invisible.
     *
     * `requestPointerLock()` allows the mouse to move forever without leaving the
     * screen. Calling `requestPointerLock()` locks the values of
     * <a href="#/p5/mouseX">mouseX</a>, <a href="#/p5/mouseY">mouseY</a>,
     * <a href="#/p5/pmouseX">pmouseX</a>, and <a href="#/p5/pmouseY">pmouseY</a>.
     * <a href="#/p5/movedX">movedX</a> and <a href="#/p5/movedY">movedY</a>
     * continue updating and can be used to get the distance the mouse moved since
     * the last frame was drawn. Calling
     * <a href="#/p5/exitPointerLock">exitPointerLock()</a> resumes updating the
     * mouse system variables.
     *
     * Note: Most browsers require an input, such as a click, before calling
     * `requestPointerLock()`. It’s recommended to call `requestPointerLock()` in
     * an event function such as <a href="#/p5/doubleClicked">doubleClicked()</a>.
     *
     * @method requestPointerLock
     *
     * @example
     * let score = 0;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with the text "Score: X" at its center. The score increases when the user moves the mouse upward. It decreases when the user moves the mouse downward.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Update the score.
     *   score -= movedY;
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the score.
     *   text(`Score: ${score}`, 50, 50);
     * }
     *
     * // Lock the pointer when the user double-clicks.
     * function doubleClicked() {
     *   requestPointerLock();
     * }
     */
    fn.requestPointerLock = function() {
      // pointer lock object forking for cross browser
      const canvas = this._curElement.elt;
      canvas.requestPointerLock =
        canvas.requestPointerLock || canvas.mozRequestPointerLock;
      if (!canvas.requestPointerLock) {
        console.log('requestPointerLock is not implemented in this browser');
        return false;
      }
      canvas.requestPointerLock();
      return true;
    };

    /**
     * Exits a pointer lock started with
     * <a href="#/p5/requestPointerLock">requestPointerLock</a>.
     *
     * Calling `requestPointerLock()` locks the values of
     * <a href="#/p5/mouseX">mouseX</a>, <a href="#/p5/mouseY">mouseY</a>,
     * <a href="#/p5/pmouseX">pmouseX</a>, and <a href="#/p5/pmouseY">pmouseY</a>.
     * Calling `exitPointerLock()` resumes updating the mouse system variables.
     *
     * Note: Most browsers require an input, such as a click, before calling
     * `requestPointerLock()`. It’s recommended to call `requestPointerLock()` in
     * an event function such as <a href="#/p5/doubleClicked">doubleClicked()</a>.
     *
     * @method exitPointerLock
     *
     * @example
     * let isLocked = false;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe(
     *     'A gray square with a word at its center. The word changes between "Unlocked" and "Locked" when the user double-clicks.'
     *   );
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Tell the user whether the pointer is locked.
     *   if (isLocked === true) {
     *     text('Locked', 50, 50);
     *   } else {
     *     text('Unlocked', 50, 50);
     *   }
     * }
     *
     * // Toggle the pointer lock when the user double-clicks.
     * function doubleClicked() {
     *   if (isLocked === true) {
     *     exitPointerLock();
     *     isLocked = false;
     *   } else {
     *     requestPointerLock();
     *     isLocked = true;
     *   }
     * }
     */
    fn.exitPointerLock = function() {
      document.exitPointerLock();
    };
  }

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

  function events(p5){
    p5.registerAddon(acceleration);
    p5.registerAddon(keyboard);
    p5.registerAddon(pointer);
  }

  function image(p5){
    p5.registerAddon(image$1);
    p5.registerAddon(loadingDisplaying);
    p5.registerAddon(image$2);
    p5.registerAddon(pixels);
    p5.registerAddon(shader);
    p5.registerAddon(texture);
  }

  /**
   * @module IO
   * @submodule Table
   * @requires core
   */


  class Table {
    constructor(rows) {
      this.columns = [];
      this.rows = [];
    }

    toString(separator=',') {
      let rows = this.rows.map(row => row.arr);

      if(!this.columns.some(column => column === null)){
        rows = [this.columns, ...rows];
      }

      return stringify(rows, {
        separator
      });
    }

    /**
     *  Use <a href="/reference/p5.Table/addRow/">addRow()</a> to add a new row of data to a <a href="#/p5.Table">p5.Table</a> object. By default,
     *  an empty row is created. Typically, you would store a reference to
     *  the new row in a TableRow object (see newRow in the example above),
     *  and then set individual values using <a href="#/p5/set">set()</a>.
     *
     *  If a <a href="#/p5.TableRow">p5.TableRow</a> object is included as a parameter, then that row is
     *  duplicated and added to the table.
     *
     *  @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     *  @param   {p5.TableRow} [row] row to be added to the table
     *  @return  {p5.TableRow} the row that was added
     *
     * @example
     * // Given the CSV file "mammals.csv"
     * // in the project's "assets" folder:
     * //
     * // id,species,name
     * // 0,Capra hircus,Goat
     * // 1,Panthera pardus,Leopard
     * // 2,Equus zebra,Zebra
     *
     * let table;
     *
     * async function setup() {
     *   // Create a 300x300 canvas
     *   createCanvas(300, 300);
     *
     *   // Load the CSV file from the assets folder with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Add a new row for "Wolf"
     *   let newRow = table.addRow();
     *   newRow.setString('id', table.getRowCount() - 1);
     *   newRow.setString('species', 'Canis Lupus');
     *     newRow.setString('name', 'Wolf');
     *
     *   // Set text properties
     *   fill(0);       // Text color: black
     *    textSize(12);  // Adjust text size as needed
     *
     *   // Display the table data on the canvas
     *   // Each cell is positioned based on its row and column
     *   for (let r = 0; r < table.getRowCount(); r++) {
     *     for (let c = 0; c < table.getColumnCount(); c++) {
     *       let x = c * 50 + 10;  // Horizontal spacing for each column
     *       let y = r * 30 + 20;  // Vertical spacing for each row
     *       text(table.getString(r, c), x * c, y);
     *     }
     *   }
     *
     *   describe('no image displayed');
     * }
     */
    addRow (row) {
    // make sure it is a valid TableRow
      const r = row || new p5.TableRow();

      if (typeof r.arr === 'undefined' || typeof r.obj === 'undefined') {
      //r = new p5.prototype.TableRow(r);
        throw new Error(`invalid TableRow: ${r}`);
      }
      r.table = this;
      this.rows.push(r);
      return r;
    }

    /**
     * Removes a row from the table object.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param   {Integer} id ID number of the row to remove
     *
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 200x200 canvas and set a white background
     *   createCanvas(200, 200);
     *   background(255);
     *
     *   // Load the CSV file with a header row
     *    table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Remove the first row from the table
     *   table.removeRow(0);
     *
     *   // Set text properties for drawing on the canvas
     *   fill(0);      // Set text color to black
     *   textSize(12); // Adjust text size as needed
     *
     *   // Display the table values on the canvas:
     *   // Each row's cell values are joined into a single string and drawn on a new line.
     *   let y = 20; // Starting vertical position
     *   for (let r = 0; r < table.getRowCount(); r++) {
     *     let rowText = "";
     *     for (let c = 0; c < table.getColumnCount(); c++) {
     *       rowText += table.getString(r, c) + " ";
     *     }
     *     text(rowText, 18, y * 3);
     *     y += 20;
     *   }
     *
     *   describe('no image displayed');
     * }
     */
    removeRow (id) {
      this.rows[id].table = null; // remove reference to table
      const chunk = this.rows.splice(id + 1, this.rows.length);
      this.rows.pop();
      this.rows = this.rows.concat(chunk);
    }

    /**
     * Returns a reference to the specified <a href="#/p5.TableRow">p5.TableRow</a>. The reference
     * can then be used to get and set values of the selected row.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param  {Integer}   rowID ID number of the row to get
     * @return {p5.TableRow} <a href="#/p5.TableRow">p5.TableRow</a> object
     *
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 200x200 canvas
     *   createCanvas(200, 200);
     *   background(255); // Set background to white
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Get the row at index 1 (second row)
     *   let row = table.getRow(1);
     *
     *   // Set text properties for drawing on the canvas
     *   fill(0);      // Set text color to black
     *   textSize(12); // Set the text size
     *
     *   // Loop over each column in the row and display its value on the canvas
     *   for (let c = 0; c < table.getColumnCount(); c++) {
     *     text(row.getString(c), 10, 20 + c * 50 + 20);
     *   }
     *
     *   describe('no image displayed');
     * }
     */
    getRow (r) {
      return this.rows[r];
    }

    /**
     * Gets all rows from the table. Returns an array of <a href="#/p5.TableRow">p5.TableRow</a>s.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @return {p5.TableRow[]}   Array of <a href="#/p5.TableRow">p5.TableRow</a>s
     *
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 200x200 canvas and set a white background
     *   createCanvas(200, 200);
     *   background(255);
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   let rows = table.getRows();
     *
     *   // Warning: rows is an array of objects.
     *   // Set the 'name' of each row to 'Unicorn'
     *   for (let r = 0; r < rows.length; r++) {
     *     rows[r].set('name', 'Unicorn');
     *   }
     *
     *   // Set text properties
     *   fill(0);      // Set text color to black
     *   textSize(12); // Adjust text size as needed
     *
     *   // Display the modified table values on the canvas
     *   // We'll join each row's values with a space and display each row on a new line.
     *   let y = 20; // Starting y position
     *   for (let r = 0; r < table.getRowCount(); r++) {
     *     let rowText = "";
     *     for (let c = 0; c < table.getColumnCount(); c++) {
     *       rowText += table.getString(r, c) + " ";
     *     }
     *     text(rowText, 10, y * 2);
     *     y += 20; // Move to next line
     *   }
     *
     *   describe('no image displayed');
     * }
     */
    getRows () {
      return this.rows;
    }

    /**
     * Finds the first row in the Table that contains the value
     * provided, and returns a reference to that row. Even if
     * multiple rows are possible matches, only the first matching
     * row is returned. The column to search may be specified by
     * either its ID or title.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param  {String} value  The value to match
     * @param  {Integer|String} column ID number or title of the
     *                                column to search
     * @return {p5.TableRow}
     *
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 100x100 canvas
     *   createCanvas(100, 100);
     *   background(255); // Set background to white
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *    // Find the row with the animal named "Zebra"
     *   let row = table.findRow('Zebra', 'name');
     *
     *   // Get the species from the found row
     *   let species = row.getString('species');
     *
     *   // Set text properties and display the species on the canvas
     *   fill(0);      // Set text color to black
     *   textSize(12); // Adjust text size as needed
     *   text(species, 10, 30);
     *
     *   describe('no image displayed');
     * }
     */
    findRow (value, column) {
    // try the Object
      if (typeof column === 'string') {
        for (let i = 0; i < this.rows.length; i++) {
          if (this.rows[i].obj[this.columns.indexOf(column)] === value) {
            return this.rows[i];
          }
        }
      } else {
      // try the Array
        for (let j = 0; j < this.rows.length; j++) {
          if (this.rows[j].arr[column] === value) {
            return this.rows[j];
          }
        }
      }
      // otherwise...
      return null;
    }

    /**
     * Finds the rows in the Table that contain the value
     * provided, and returns references to those rows. Returns an
     * Array, so for must be used to iterate through all the rows,
     * as shown in the example above. The column to search may be
     * specified by either its ID or title.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param  {String} value  The value to match
     * @param  {Integer|String} column ID number or title of the
     *                                column to search
     * @return {p5.TableRow[]}        An Array of TableRow objects
     *
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 200x200 canvas
     *   createCanvas(200, 200);
     *   background(255); // Set background to white
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Add another goat entry
     *   let newRow = table.addRow();
     *   newRow.setString('id', table.getRowCount() - 1);
     *   newRow.setString('species', 'Scape Goat');
     *   newRow.setString('name', 'Goat');
     *
     *   // Find rows where the name is "Goat"
     *   let rows = table.findRows('Goat', 'name');
     *
     *   // Set text properties
     *   fill(0);      // Set text color to black
     *   textSize(12); // Adjust text size as needed
     *
     *   // Display the result on the canvas
     *   text(rows.length + ' Goats found', 10, 30);
     *
     *   describe('no image displayed');
     * }
     */
    findRows (value, column) {
      const ret = [];
      if (typeof column === 'string') {
        for (let i = 0; i < this.rows.length; i++) {
          if (this.rows[i].obj[this.columns.indexOf(column)] === value) {
            ret.push(this.rows[i]);
          }
        }
      } else {
      // try the Array
        for (let j = 0; j < this.rows.length; j++) {
          if (this.rows[j].arr[column] === value) {
            ret.push(this.rows[j]);
          }
        }
      }
      return ret;
    }

    /**
     * Finds the first row in the Table that matches the regular
     * expression provided, and returns a reference to that row.
     * Even if multiple rows are possible matches, only the first
     * matching row is returned. The column to search may be
     * specified by either its ID or title.
     *
     *  @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param  {String|RegExp} regexp The regular expression to match
     * @param  {String|Integer} column The column ID (number) or
     *                                  title (string)
     * @return {p5.TableRow}        TableRow object
     *
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 200x200 canvas
     *   createCanvas(200, 200);
     *   background(255); // Set background to white
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Search using the specified regex on column index 1 (species)
     *   let mammal = table.matchRow(new RegExp('ant'), 1);
     *   let species = mammal.getString(1);  // "Panthera pardus"
     *
     *   // Set text properties for drawing on the canvas
     *   fill(0);       // Text color: black
     *   textSize(12);  // Adjust text size as needed
     *
     *   // Display the species on the canvas
     *   text(species, 10, 30);
     *
     *   describe('no image displayed');
     * }
     */
    matchRow (regexp, column) {
      if (typeof column === 'number') {
        for (let j = 0; j < this.rows.length; j++) {
          if (this.rows[j].arr[column].match(regexp)) {
            return this.rows[j];
          }
        }
      } else {
        for (let i = 0; i < this.rows.length; i++) {
          if (this.rows[i].obj[this.columns.indexOf(column)].match(regexp)) {
            return this.rows[i];
          }
        }
      }
      return null;
    }

    /**
     * Finds the rows in the Table that match the regular expression provided,
     * and returns references to those rows. Returns an array, so for must be
     * used to iterate through all the rows, as shown in the example. The
     * column to search may be specified by either its ID or title.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param  {String} regexp The regular expression to match
     * @param  {String|Integer} [column] The column ID (number) or
     *                                  title (string)
     * @return {p5.TableRow[]}          An Array of TableRow objects
     * @example
     * let table;
     *
     * function setup() {
     *   // Create a 200x200 canvas and set a white background
     *   createCanvas(200, 200);
     *   background(255);
     *
     *   // Create a new p5.Table and add columns
     *   table = new p5.Table();
     *   table.addColumn('name');
     *   table.addColumn('type');
     *
     *   // Add rows to the table
     *   let newRow = table.addRow();
     *   newRow.setString('name', 'Lion');
     *   newRow.setString('type', 'Mammal');
     *
     *   newRow = table.addRow();
     *   newRow.setString('name', 'Snake');
     *   newRow.setString('type', 'Reptile');
     *
     *   newRow = table.addRow();
     *   newRow.setString('name', 'Mosquito');
     *   newRow.setString('type', 'Insect');
     *
     *   newRow = table.addRow();
     *   newRow.setString('name', 'Lizard');
     *   newRow.setString('type', 'Reptile');
     *
     *   // Search for rows where the "type" starts with "R"
     *   let rows = table.matchRows('R.*', 'type');
     *
     *   // Set text properties for drawing on the canvas
     *   fill(0);       // Text color: black
     *   textSize(12);  // Text size
     *
     *   // Display each matching row on the canvas
     *   let y = 20;
     *   for (let i = 0; i < rows.length; i++) {
     *     let output = rows[i].getString('name') + ': ' + rows[i].getString('type');
     *     text(output, 10, y);
     *     y += 20;
     *   }
     * }
     */
    matchRows (regexp, column) {
      const ret = [];
      if (typeof column === 'number') {
        for (let j = 0; j < this.rows.length; j++) {
          if (this.rows[j].arr[column].match(regexp)) {
            ret.push(this.rows[j]);
          }
        }
      } else {
        for (let i = 0; i < this.rows.length; i++) {
          if (this.rows[i].obj[this.columns.indexOf(column)].match(regexp)) {
            ret.push(this.rows[i]);
          }
        }
      }
      return ret;
    }

    /**
     *  Retrieves all values in the specified column, and returns them
     *  as an array. The column may be specified by either its ID or title.
     *
     *  @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     *  @param  {String|Number} column String or Number of the column to return
     *  @return {Array}       Array of column values
     *
     * @example
     * // META:norender
     * // Given the CSV file "mammals.csv"
     * // in the project's "assets" folder:
     * //
     * // id,species,name
     * // 0,Capra hircus,Goat
     * // 1,Panthera pardus,Leopard
     * // 2,Equus zebra,Zebra
     *
     * let table;
     *
     * async function setup() {
     *   // The table is comma separated value "csv"
     *   // and has a header specifying the columns labels.
     *   table = await loadTable('assets/mammals.csv', 'csv', 'header');
     *
     *   //getColumn returns an array that can be printed directly
     *   print(table.getColumn('species'));
     *   //outputs ["Capra hircus", "Panthera pardus", "Equus zebra"]
     *   describe('no image displayed');
     * }
     */
    getColumn (value) {
      const ret = [];
      if (typeof value === 'string') {
        for (let i = 0; i < this.rows.length; i++) {
          ret.push(this.rows[i].obj[this.columns.indexOf(value)]);
        }
      } else {
        for (let j = 0; j < this.rows.length; j++) {
          ret.push(this.rows[j].arr[value]);
        }
      }
      return ret;
    }

    /**
     * Removes all rows from a Table. While all rows are removed,
     * columns and column titles are maintained.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     *
     * @example
     * // Given the CSV file "mammals.csv"
     * // in the project's "assets" folder:
     * //
     * // id,species,name
     * // 0,Capra hircus,Goat
     * // 1,Panthera pardus,Leopard
     * // 2,Equus zebra,Zebra
     *
     * let table;
     *
     * async function setup() {
     *   // Create a 200x200 canvas
     *   createCanvas(200, 200);
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Clear all rows from the table
     *   table.clearRows();
     *
     *   // Set text properties
     *   fill(0);       // Text color: black
     *   textSize(12);  // Adjust text size as needed
     *
     *   // Display the number of rows and columns on the canvas
     *   text(table.getRowCount() + ' total rows in table', 10, 30);
     *   text(table.getColumnCount() + ' total columns in table', 10, 60);
     *
     *   describe('no image displayed');
     * }
     */
    clearRows () {
      delete this.rows;
      this.rows = [];
    }

    /**
     * Use <a href="/reference/p5.Table/addColumn/">addColumn()</a> to add a new column to a <a href="#/p5.Table">Table</a> object.
     * Typically, you will want to specify a title, so the column
     * may be easily referenced later by name. (If no title is
     * specified, the new column's title will be null.)
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param {String} [title] title of the given column
     *
     * @example
     * let table;
     *
     * async function setup() {
     *   createCanvas(300, 300);
     *   table = await loadTable('/assets/mammals.csv', ',', 'header');
     *
     *   table.addColumn('carnivore');
     *   table.set(0, 'carnivore', 'no');
     *   table.set(1, 'carnivore', 'yes');
     *   table.set(2, 'carnivore', 'no');
     *
     *   fill(0);      // Set text color to black
     *   textSize(11); // Adjust text size as needed
     *
     *   for (let r = 0; r < table.getRowCount(); r++) {
     *     for (let c = 0; c < table.getColumnCount(); c++) {
     *       // Keep column spacing consistent (e.g. 80 pixels apart).
     *       let x = c * 80 + 10;
     *       let y = r * 30 + 20;
     *       // Use x directly, rather than multiplying by c again
     *       text(table.getString(r, c), x, y);
     *     }
     *   }
     *
     *   describe('no image displayed');
     * }
     */
    addColumn (title) {
      const t = title || null;
      this.columns.push(t);
    }

    /**
     * Returns the total number of columns in a Table.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @return {Integer} Number of columns in this table
     * @example
     * // given the cvs file "blobs.csv" in /assets directory
     * // ID, Name, Flavor, Shape, Color
     * // Blob1, Blobby, Sweet, Blob, Pink
     * // Blob2, Saddy, Savory, Blob, Blue
     *
     * let table;
     *
     * async function setup() {
     *   table = await loadTable('assets/blobs.csv');
     *
     *   createCanvas(200, 100);
     *   textAlign(CENTER);
     *   background(255);
     * }
     *
     * function draw() {
     *   let numOfColumn = table.getColumnCount();
     *   text('There are ' + numOfColumn + ' columns in the table.', 100, 50);
     * }
     */
    getColumnCount () {
      return this.columns.length;
    }

    /**
     * Returns the total number of rows in a Table.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @return {Integer} Number of rows in this table
     * @example
     * // given the cvs file "blobs.csv" in /assets directory
     * //
     * // ID, Name, Flavor, Shape, Color
     * // Blob1, Blobby, Sweet, Blob, Pink
     * // Blob2, Saddy, Savory, Blob, Blue
     *
     * let table;
     *
     * async function setup() {
     *   table = await loadTable('assets/blobs.csv');
     *
     *   createCanvas(200, 100);
     *   textAlign(CENTER);
     *   background(255);
     * }
     *
     * function draw() {
     *   text('There are ' + table.getRowCount() + ' rows in the table.', 100, 50);
     * }
     */
    getRowCount () {
      return this.rows.length;
    }

    /**
     * Removes any of the specified characters (or "tokens").
     *
     * If no column is specified, then the values in all columns and
     * rows are processed. A specific column may be referenced by
     * either its ID or title.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param  {String} chars  String listing characters to be removed
     * @param  {String|Integer} [column] Column ID (number)
     *                                   or name (string)
     *
     * @example
     * // META:norender
     * function setup() {
     *   let table = new p5.Table();
     *
     *   table.addColumn('name');
     *   table.addColumn('type');
     *
     *   let newRow = table.addRow();
     *   newRow.setString('name', '   $Lion  ,');
     *   newRow.setString('type', ',,,Mammal');
     *
     *   newRow = table.addRow();
     *   newRow.setString('name', '$Snake  ');
     *   newRow.setString('type', ',,,Reptile');
     *
     *   table.removeTokens(',$ ');
     *   print(table.getArray());
     * }
     *
     * // prints:
     * //  0  "Lion"   "Mamal"
     * //  1  "Snake"  "Reptile"
     */
    removeTokens (chars, column) {
      const escape = s => s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
      const charArray = [];
      for (let i = 0; i < chars.length; i++) {
        charArray.push(escape(chars.charAt(i)));
      }
      const regex = new RegExp(charArray.join('|'), 'g');

      if (typeof column === 'undefined') {
        for (let c = 0; c < this.columns.length; c++) {
          for (let d = 0; d < this.rows.length; d++) {
            let s = this.rows[d].arr[c];
            s = s.replace(regex, '');
            this.rows[d].arr[c] = s;
            this.rows[d].obj[this.columns[c]] = s;
          }
        }
      } else if (typeof column === 'string') {
        for (let j = 0; j < this.rows.length; j++) {
          let val = this.rows[j].obj[column];
          val = val.replace(regex, '');
          this.rows[j].obj[column] = val;
          const pos = this.columns.indexOf(column);
          this.rows[j].arr[pos] = val;
        }
      } else {
        for (let k = 0; k < this.rows.length; k++) {
          let str = this.rows[k].arr[column];
          str = str.replace(regex, '');
          this.rows[k].arr[column] = str;
          this.rows[k].obj[this.columns[column]] = str;
        }
      }
    }

    /**
     * Trims leading and trailing whitespace, such as spaces and tabs,
     * from String table values. If no column is specified, then the
     * values in all columns and rows are trimmed. A specific column
     * may be referenced by either its ID or title.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param  {String|Integer} [column] Column ID (number)
     *                                   or name (string)
     * @example
     * // META:norender
     * function setup() {
     *   let table = new p5.Table();
     *
     *   table.addColumn('name');
     *   table.addColumn('type');
     *
     *   let newRow = table.addRow();
     *   newRow.setString('name', '   Lion  ,');
     *   newRow.setString('type', ' Mammal  ');
     *
     *   newRow = table.addRow();
     *   newRow.setString('name', '  Snake  ');
     *   newRow.setString('type', '  Reptile  ');
     *
     *   table.trim();
     *   print(table.getArray());
     * }
     *
     * // prints:
     * //  0  "Lion"   "Mamal"
     * //  1  "Snake"  "Reptile"
     */
    trim (column) {
      const regex = new RegExp(' ', 'g');

      if (typeof column === 'undefined') {
        for (let c = 0; c < this.columns.length; c++) {
          for (let d = 0; d < this.rows.length; d++) {
            let s = this.rows[d].arr[c];
            s = s.replace(regex, '');
            this.rows[d].arr[c] = s;
            this.rows[d].obj[this.columns[c]] = s;
          }
        }
      } else if (typeof column === 'string') {
        for (let j = 0; j < this.rows.length; j++) {
          let val = this.rows[j].obj[column];
          val = val.replace(regex, '');
          this.rows[j].obj[column] = val;
          const pos = this.columns.indexOf(column);
          this.rows[j].arr[pos] = val;
        }
      } else {
        for (let k = 0; k < this.rows.length; k++) {
          let str = this.rows[k].arr[column];
          str = str.replace(regex, '');
          this.rows[k].arr[column] = str;
          this.rows[k].obj[this.columns[column]] = str;
        }
      }
    }

    /**
     * Use <a href="/reference/p5.Table/removeColumn/">removeColumn()</a> to remove an existing column from a Table
     * object. The column to be removed may be identified by either
     * its title (a String) or its index value (an int).
     * removeColumn(0) would remove the first column, removeColumn(1)
     * would remove the second column, and so on.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param  {String|Integer} column columnName (string) or ID (number)
     *
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 100x100 canvas
     *   createCanvas(100, 100);
     *   background(255); // Set background to white
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *    // Remove the "id" column
     *    table.removeColumn('id');
     *
     *   // Get the remaining column count
     *   let colCount = table.getColumnCount();
     *
     *   // Set text properties
     *   fill(0);      // Text color: black
     *   textSize(12); // Adjust text size as needed
     *
     *   // Display the column count on the canvas
     *   text(colCount, 40, 50);
     *
     *   describe('no image displayed');
     * }
     */
    removeColumn (c) {
      let cString;
      let cNumber;
      if (typeof c === 'string') {
      // find the position of c in the columns
        cString = c;
        cNumber = this.columns.indexOf(c);
      } else {
        cNumber = c;
        cString = this.columns[c];
      }

      const chunk = this.columns.splice(cNumber + 1, this.columns.length);
      this.columns.pop();
      this.columns = this.columns.concat(chunk);

      for (let i = 0; i < this.rows.length; i++) {
        const tempR = this.rows[i].arr;
        const chip = tempR.splice(cNumber + 1, tempR.length);
        tempR.pop();
        this.rows[i].arr = tempR.concat(chip);
        delete this.rows[i].obj[cString];
      }
    }

    /**
     * Stores a value in the Table's specified row and column.
     * The row is specified by its ID, while the column may be specified
     * by either its ID or title.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param {Integer} row row ID
     * @param {String|Integer} column column ID (Number)
     *                               or title (String)
     * @param {String|Number} value  value to assign
     *
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 200x200 canvas and set a white background
     *   createCanvas(200, 200);
     *   background(255);
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Update the first row: change species to "Canis Lupus" and name to "Wolf"
     *   table.set(0, 'species', 'Canis Lupus');
     *   table.set(0, 'name', 'Wolf');
     *
     *   // Set text properties for drawing on the canvas
     *   fill(0);      // Text color: black
     *    textSize(12); // Adjust text size as needed
     *
     *   // Display the table values on the canvas:
     *   // Each row's values are concatenated into a single string and displayed on a new line.
     *   let y = 20; // Starting vertical position
     *   for (let r = 0; r < table.getRowCount(); r++) {
     *     let rowText = "";
     *     for (let c = 0; c < table.getColumnCount(); c++) {
     *       rowText += table.getString(r, c) + " ";
     *     }
     *     text(rowText, 10, y * 2.5);
     *     y += 20;
     *   }
     *
     *   describe('no image displayed');
     * }
     */
    set (row, column, value) {
      this.rows[row].set(column, value);
    }

    /**
     * Stores a Float value in the Table's specified row and column.
     * The row is specified by its ID, while the column may be specified
     * by either its ID or title.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param {Integer} row row ID
     * @param {String|Integer} column column ID (Number)
     *                               or title (String)
     * @param {Number} value  value to assign
     *
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 100x100 canvas and set a white background
     *   createCanvas(100, 100);
     *   background(255);
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Set the value in row 1, column "id" to the number 1
     *   table.setNum(1, 'id', 1);
     *
     *   // Get the first column as an array and join its values into a string for display.
     *   let col0 = table.getColumn(0);  // Expected output: ["0", 1, "2"]
     *   let output = col0.join(", ");
     *
     *   // Set text properties and display the output on the canvas
     *   fill(0);      // Text color: black
     *   textSize(12); // Adjust text size as needed
     *   text(output, 30, 50);
     *
     *   describe('no image displayed');
     * }
     */
    setNum (row, column, value) {
      this.rows[row].setNum(column, value);
    }

    /**
     * Stores a String value in the Table's specified row and column.
     * The row is specified by its ID, while the column may be specified
     * by either its ID or title.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param {Integer} row row ID
     * @param {String|Integer} column column ID (Number)
     *                               or title (String)
     * @param {String} value  value to assign
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 200x200 canvas and set a white background
     *   createCanvas(200, 200);
     *   background(255);
     *
     *   // Load the CSV file from the assets folder with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Add a new row with the new animal data
     *   let newRow = table.addRow();
     *   newRow.setString('id', table.getRowCount() - 1);
     *   newRow.setString('species', 'Canis Lupus');
     *   newRow.setString('name', 'Wolf');
     *
     *   // Convert the table to a 2D array
     *   let tableArray = table.getArray();
     *
     *   // Set text properties
     *   fill(0);       // Set text color to black
     *   textSize(12);  // Adjust text size as needed
     *
     *   // Display each row of the table on the canvas
     *   let y = 20;  // Starting y position
     *   for (let i = 0; i < tableArray.length; i++) {
     *     // Join the values of each row with a comma separator
     *     let rowText = tableArray[i].join(', ');
     *     text(rowText, 15, y * 2);
     *     y += 20;  // Increment y position for the next row
     *   }
     *
     *   describe('no image displayed');
     * }
     */
    setString (row, column, value) {
      this.rows[row].setString(column, value);
    }

    /**
     * Retrieves a value from the Table's specified row and column.
     * The row is specified by its ID, while the column may be specified by
     * either its ID or title.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param {Integer} row row ID
     * @param  {String|Integer} column columnName (string) or
     *                                   ID (number)
     * @return {String|Number}
     *
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 100x100 canvas
     *   createCanvas(100, 100);
     *   background(255); // Set background to white
     *
     *   // Load the CSV file from the assets folder with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Set text properties for drawing on the canvas
     *   fill(0);      // Text color: black
     *   textSize(12); // Adjust text size as needed
     *
     *   // Get the values from the table
     *   let value1 = table.get(0, 1);       // Using column index (1) => "Capra hircus"
     *   let value2 = table.get(0, 'species'); // Using column name => "Capra hircus"
     *
     *   // Display the values on the canvas
     *   text(value1, 10, 30);
     *   text(value2, 10, 60);
     *
     *   describe('no image displayed');
     * }
     */
    get (row, column) {
      if(typeof column === 'string'){
        return this.rows[row].get(this.columns.indexOf(column));
      } else {
        return this.rows[row].get(column);
      }
    }

    /**
     * Retrieves a Float value from the Table's specified row and column.
     * The row is specified by its ID, while the column may be specified by
     * either its ID or title.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param {Integer} row row ID
     * @param  {String|Integer} column columnName (string) or
     *                                   ID (number)
     * @return {Number}
     *
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 100x100 canvas
     *   createCanvas(100, 100);
     *   background(255); // Set background to white
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Compute the result: id at row 1, column 0 plus 100 (i.e. 1 + 100 = 101)
     *   let result = table.getNum(1, 0) + 100;
     *
     *   // Set text properties and display the result on the canvas
     *   fill(0);      // Set text color to black
     *   textSize(12); // Adjust text size as needed
     *   text(result, 10, 30);  // Display the result at position (10, 30)
     *
     *   describe('no image displayed');
     * }
     */
    getNum (row, column) {
      return this.rows[row].getNum(column);
    }

    /**
     * Retrieves a String value from the Table's specified row and column.
     * The row is specified by its ID, while the column may be specified by
     * either its ID or title.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param {Integer} row row ID
     * @param  {String|Integer} column columnName (string) or
     *                                   ID (number)
     * @return {String}
     *
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 200x200 canvas
     *   createCanvas(200, 200);
     *   background(255); // Set background to white
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Set text properties
     *   fill(0);      // Text color: black
     *   textSize(12); // Adjust text size as needed
     *
     *   // Display each table cell value on the canvas one below the other.
     *   // We use a variable 'y' to increment the vertical position.
     *   let y = 20;
     *   text(table.getString(0, 0), 10, y); // 0
     *   y += 20;
     *   text(table.getString(0, 1), 10, y); // Capra hircus
     *   y += 20;
     *   text(table.getString(0, 2), 10, y); // Goat
     *   y += 20;
     *   text(table.getString(1, 0), 10, y); // 1
     *   y += 20;
     *   text(table.getString(1, 1), 10, y); // Panthera pardus
     *   y += 20;
     *   text(table.getString(1, 2), 10, y); // Leopard
     *   y += 20;
     *   text(table.getString(2, 0), 10, y); // 2
     *   y += 20;
     *   text(table.getString(2, 1), 10, y); // Equus zebra
     *   y += 20;
     *   text(table.getString(2, 2), 10, y); // Zebra
     *
     *   describe('no image displayed');
     * }
     */
    getString (row, column) {
      return this.rows[row].getString(column);
    }

    /**
     * Retrieves all table data and returns as an object. If a column name is
     * passed in, each row object will be stored with that attribute as its
     * title.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param {String} [headerColumn] Name of the column which should be used to
     *                              title each row object (optional)
     * @return {Object}
     *
     * @example
     * // META:norender
     * // Given the CSV file "mammals.csv"
     * // in the project's "assets" folder:
     * //
     * // id,species,name
     * // 0,Capra hircus,Goat
     * // 1,Panthera pardus,Leopard
     * // 2,Equus zebra,Zebra
     *
     * let table;
     *
     * async function setup() {
     *   // The table is comma separated value "csv"
     *   // and has a header specifying the columns labels.
     *   table = await loadTable('assets/mammals.csv', 'csv', 'header');
     *
     *   let tableObject = table.getObject();
     *
     *   print(tableObject);
     *   //outputs an object
     *
     *   describe('no image displayed');
     * }
     */
    getObject (headerColumn) {
      const tableObject = {};
      let obj, cPos, index;

      for (let i = 0; i < this.rows.length; i++) {
        obj = this.rows[i].obj;

        if (typeof headerColumn === 'string') {
          cPos = this.columns.indexOf(headerColumn); // index of columnID
          if (cPos >= 0) {
            index = obj[headerColumn];
            tableObject[index] = obj;
          } else {
            throw new Error(`This table has no column named "${headerColumn}"`);
          }
        } else {
          tableObject[i] = this.rows[i].obj;
        }
      }
      return tableObject;
    }

    /**
     * Retrieves all table data and returns it as a multidimensional array.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @return {Array}
     *
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 200x200 canvas and set a white background
     *   createCanvas(200, 200);
     *   background(255);
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Get the CSV data as a 2D array
     *   let tableArray = table.getArray();
     *
     *   // Set text properties
     *   fill(0);      // Set text color to black
     *   textSize(12); // Adjust text size as needed
     *
     *   // Display each row of the CSV on the canvas
     *   // Each row is displayed on a separate line
     *   for (let i = 0; i < tableArray.length; i++) {
     *     let rowText = tableArray[i].join(", ");
     *     text(rowText, 10, 20 + i * 50 + 30);
     *   }
     *
     *   describe('no image displayed');
     * }
     */
    getArray () {
      const tableArray = [];
      for (let i = 0; i < this.rows.length; i++) {
        tableArray.push(this.rows[i].arr);
      }
      return tableArray;
    }
  }
  function table(p5, fn){
    /**
     *  Table Options
     *  Generic class for handling tabular data, typically from a
     *  CSV, TSV, or other sort of spreadsheet file.
     *  CSV files are
     *  <a href="http://en.wikipedia.org/wiki/Comma-separated_values">
     *  comma separated values</a>, often with the data in quotes. TSV
     *  files use tabs as separators, and usually don't bother with the
     *  quotes.
     *  File names should end with .csv if they're comma separated.
     *  A rough "spec" for CSV can be found
     *  <a href="http://tools.ietf.org/html/rfc4180">here</a>.
     *  To load files, use the <a href="#/p5/loadTable">loadTable</a> method.
     *  To save tables to your computer, use the <a href="#/p5/save">save</a> method
     *   or the <a href="#/p5/saveTable">saveTable</a> method.
     *
     *  Possible options include:
     *  <ul>
     *  <li>csv - parse the table as comma-separated values
     *  <li>tsv - parse the table as tab-separated values
     *  <li>header - this table has a header (title) row
     *  </ul>
     */

    /**
     *  <a href="#/p5.Table">Table</a> objects store data with multiple rows and columns, much
     *  like in a traditional spreadsheet. Tables can be generated from
     *  scratch, dynamically, or using data from an existing file.
     *
     *  @class p5.Table
     *  @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     *  @param  {p5.TableRow[]}     [rows] An array of p5.TableRow objects
     */
    p5.Table = Table;

    /**
     * An array containing the names of the columns in the table, if the "header" the table is
     * loaded with the "header" parameter.
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @type {String[]}
     * @property columns
     * @for p5.Table
     * @name columns
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 200x200 canvas
     *   createCanvas(200, 200);
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Set text properties for drawing on the canvas
     *   fill(0);       // Set text color to black
     *   textSize(12);  // Adjust text size as needed
     *
     *   // Display the column names on the canvas
     *   for (let c = 0; c < table.getColumnCount(); c++) {
     *     text('column ' + c + ' is named ' + table.columns[c], 10, 30 + c * 20);
     *   }
     * }
     */

    /**
     * An array containing the <a href="#/p5.Table">p5.TableRow</a> objects that make up the
     * rows of the table. The same result as calling <a href="/reference/p5.Table/getRows/">getRows()</a>
     * @type {p5.TableRow[]}
     * @property rows
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @for p5.Table
     * @name rows
     */
  }

  if(typeof p5 !== 'undefined'){
    table(p5);
  }

  /**
   * @module IO
   * @submodule Table
   * @requires core
   */

  class TableRow {
    constructor(row=[]){
      let arr = row;

      this.arr = arr;
      this.obj = Object.fromEntries(arr.entries());
      this.table = null;
    }

    /**
     * Stores a value in the TableRow's specified column.
     * The column may be specified by either its ID or title.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param {String|Integer} column Column ID (Number)
     *                               or Title (String)
     * @param {String|Number} value  The value to be stored
     *
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 200x200 canvas and set a white background
     *   createCanvas(200, 200);
     *   background(255);
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Set every row's "name" to "Unicorn"
     *    let rows = table.getRows();
     *   for (let r = 0; r < rows.length; r++) {
     *     rows[r].set('name', 'Unicorn');
     *   }
     *
     *   // Convert the table to an array
     *   let tableArray = table.getArray();
     *
     *   // Set text properties
     *   fill(0);      // Set text color to black
     *   textSize(12); // Set text size
     *
     *   // Display each row of the table on the canvas
     *   let y = 20; // Starting y position
     *   for (let i = 0; i < tableArray.length; i++) {
     *     let rowText = tableArray[i].join(', ');
     *     text(rowText, 10, y * 2.5);
     *     y += 20; // Increment y position for the next row
     *   }
     *
     *   describe('no image displayed');
     * }
     */
    set(column, value) {
    // if typeof column is string, use .obj
      if (typeof column === 'string') {
        const cPos = this.table.columns.indexOf(column); // index of columnID
        if (cPos >= 0) {
          this.obj[column] = value;
          this.arr[cPos] = value;
        } else {
          throw new Error(`This table has no column named "${column}"`);
        }
      } else {
      // if typeof column is number, use .arr
        if (column < this.table.columns.length) {
          this.arr[column] = value;
          const cTitle = this.table.columns[column];
          this.obj[cTitle] = value;
        } else {
          throw new Error(`Column #${column} is out of the range of this table`);
        }
      }
    }

    /**
     * Stores a Float value in the TableRow's specified column.
     * The column may be specified by either its ID or title.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param {String|Integer} column Column ID (Number)
     *                               or Title (String)
     * @param {Number|String} value  The value to be stored
     *                                as a Float
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 200x200 canvas and set a white background
     *   createCanvas(200, 200);
     *   background(255);
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Update each row's "id" to (row index + 10)
     *   let rows = table.getRows();
     *   for (let r = 0; r < rows.length; r++) {
     *     rows[r].setNum('id', r + 10);
     *   }
     *
     *   // Convert the table to a 2D array for display
     *   let tableArray = table.getArray();
     *
     *   // Set text properties
     *   fill(0);      // Text color: black
     *   textSize(12); // Adjust text size as needed
     *
     *   // Display each row of the table on the canvas
     *   let y = 20;   // Starting y position
     *   for (let i = 0; i < tableArray.length; i++) {
     *     // Join each row's values with a comma separator
     *     let rowText = tableArray[i].join(', ');
     *     text(rowText, 10, y * 2.5);
     *     y += 20;  // Increment y for the next row
     *   }
     *
     *   describe('no image displayed');
     * }
     */
    setNum(column, value) {
      const floatVal = parseFloat(value);
      this.set(column, floatVal);
    }

    /**
     * Stores a String value in the TableRow's specified column.
     * The column may be specified by either its ID or title.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param {String|Integer} column Column ID (Number)
     *                               or Title (String)
     * @param {String|Number|Boolean|Object} value  The value to be stored
     *                                as a String
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 300x200 canvas and set a white background
     *   createCanvas(300, 200);
     *   background(255);
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Update each row's "name" field
     *   let rows = table.getRows();
     *   for (let r = 0; r < rows.length; r++) {
     *     let name = rows[r].getString('name');
     *     rows[r].setString('name', 'A ' + name + ' named George');
     *   }
     *
     *   // Convert the table to a 2D array for display
     *   let tableArray = table.getArray();
     *
     *   // Set text properties
     *   fill(0);      // Text color: black
     *   textSize(12); // Adjust text size as needed
     *
     *   // Display each row of the table on the canvas
     *   let y = 20;   // Starting y position
     *   for (let i = 0; i < tableArray.length; i++) {
     *     let rowText = tableArray[i].join(', ');
     *     text(rowText, 10, y * 2.5);
     *     y += 20;    // Increment y for the next row
     *   }
     *
     *   // describe('no image displayed');
     * }
     */
    setString(column, value) {
      const stringVal = value.toString();
      this.set(column, stringVal);
    }

    /**
     * Retrieves a value from the TableRow's specified column.
     * The column may be specified by either its ID or title.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param  {String|Integer} column columnName (string) or
     *                                  ID (number)
     * @return {String|Number}
     *
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 200x100 canvas and set a white background
     *   createCanvas(200, 100);
     *   background(255);
     *
     *   // Load the CSV file with a header row
     *    table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   // Extract the names from each row and store them in an array
     *   let names = [];
     *   let rows = table.getRows();
     *   for (let r = 0; r < rows.length; r++) {
     *     names.push(rows[r].get('name'));
     *   }
     *
     *   // Set text properties and display the names on the canvas
     *   fill(0);      // Set text color to black
     *   textSize(12); // Set text size
     *
     *   // Join names into a single string separated by commas
     *   let namesText = names.join(', ');
     *   text(namesText, 35, 50);
     *
     *   describe('no image displayed');
     * }
     */
    get(column) {
      if (typeof column === 'string') {
        return this.obj[this.table.columns.indexOf(column)];
      } else {
        return this.arr[column];
      }
    }

    /**
     * Retrieves a Float value from the TableRow's specified
     * column. The column may be specified by either its ID or
     * title.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param  {String|Integer} column columnName (string) or
     *                                  ID (number)
     * @return {Number}  Float Floating point number
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 300x100 canvas and set a white background
     *   createCanvas(300, 100);
     *   background(255);
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   let rows = table.getRows();
     *   let minId = Infinity;
     *   let maxId = -Infinity;
     *
     *   for (let r = 0; r < rows.length; r++) {
     *     let id = rows[r].getNum('id');
     *     minId = min(minId, id);
     *     maxId = max(maxId, id);
     *    }
     *
     *   let result = 'minimum id = ' + minId + ', maximum id = ' + maxId;
     *
     *   // Set text properties and display the result on the canvas
     *   fill(0);      // Set text color to black
     *   textSize(12); // Set text size
     *   text(result, 10, 50);
     *
     *   describe('no image displayed');
     * }
     */
    getNum(column) {
      let ret;
      if (typeof column === 'string') {
        ret = parseFloat(this.obj[this.table.columns.indexOf(column)]);
      } else {
        ret = parseFloat(this.arr[column]);
      }

      if (ret.toString() === 'NaN') {
        throw `Error: ${this.obj[column]} is NaN (Not a Number)`;
      }
      return ret;
    }

    /**
     * Retrieves an String value from the TableRow's specified
     * column. The column may be specified by either its ID or
     * title.
     *
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     *
     * @param  {String|Integer} column columnName (string) or
     *                                  ID (number)
     * @return {String}  String
     * @example
     * let table;
     *
     * async function setup() {
     *   // Create a 200x100 canvas and set a white background
     *   createCanvas(200, 100);
     *   background(255);
     *
     *   // Load the CSV file with a header row
     *   table = await loadTable('assets/mammals.csv', ',', 'header');
     *
     *   let rows = table.getRows();
     *   let longest = '';
     *   for (let r = 0; r < rows.length; r++) {
     *      let species = rows[r].getString('species');
     *      if (longest.length < species.length) {
     *       longest = species;
     *     }
     *   }
     *
     *   let result = 'longest: ' + longest;
     *
     *   // Set text properties and display the result on the canvas
     *   fill(0);      // Set text color to black
     *   textSize(12); // Set text size
     *   text(result, 30, 50);
     *
     *   describe('no image displayed');
     * }
     */
    getString(column) {
      if (typeof column === 'string') {
        return this.obj[this.table.columns.indexOf(column)].toString();
      } else {
        return this.arr[column].toString();
      }
    }
  }
  function tableRow(p5, fn){
    /**
     * A TableRow object represents a single row of data values,
     * stored in columns, from a table.
     *
     * A Table Row contains both an ordered array, and an unordered
     * JSON object.
     *
     * @class p5.TableRow
     * @deprecated p5.Table will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @constructor
     * @param {any[]} row         optional: populate the row with an
     *                             array of values
     */
    p5.TableRow = TableRow;
  }

  if(typeof p5 !== 'undefined'){
    tableRow(p5);
  }

  function io(p5){
    p5.registerAddon(files);
    p5.registerAddon(table);
    p5.registerAddon(tableRow);
    p5.registerAddon(xml);
  }

  /**
   * @module Math
   * @submodule Calculation
   * @for p5
   * @requires core
   */

  function calculation(p5, fn){
    /**
     * Calculates the absolute value of a number.
     *
     * A number's absolute value is its distance from zero on the number line.
     * -5 and 5 are both five units away from zero, so calling `abs(-5)` and
     * `abs(5)` both return 5. The absolute value of a number is always positive.
     *
     * @method abs
     * @param  {Number} n number to compute.
     * @return {Number}   absolute value of given number.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A gray square with a vertical black line that divides it in half. A white rectangle gets taller when the user moves the mouse away from the line.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Divide the canvas.
     *   line(50, 0, 50, 100);
     *
     *   // Calculate the mouse's distance from the middle.
     *   let h = abs(mouseX - 50);
     *
     *   // Draw a rectangle based on the mouse's distance
     *   // from the middle.
     *   rect(0, 100 - h, 100, h);
     * }
     */
    fn.abs = Math.abs;

    /**
     * Calculates the closest integer value that is greater than or equal to a
     * number.
     *
     * For example, calling `ceil(9.03)` and `ceil(9.97)` both return the value
     * 10.
     *
     * @method ceil
     * @param  {Number} n number to round up.
     * @return {Integer}   rounded up number.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Use RGB color with values from 0 to 1.
     *   colorMode(RGB, 1);
     *
     *   noStroke();
     *
     *   // Draw the left rectangle.
     *   let r = 0.3;
     *   fill(r, 0, 0);
     *   rect(0, 0, 50, 100);
     *
     *   // Round r up to 1.
     *   r = ceil(r);
     *
     *   // Draw the right rectangle.
     *   fill(r, 0, 0);
     *   rect(50, 0, 50, 100);
     *
     *   describe('Two rectangles. The one on the left is dark red and the one on the right is bright red.');
     * }
     */
    fn.ceil = Math.ceil;

    /**
     * Constrains a number between a minimum and maximum value.
     *
     * @method constrain
     * @param  {Number} n    number to constrain.
     * @param  {Number} low  minimum limit.
     * @param  {Number} high maximum limit.
     * @return {Number}      constrained number.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A black dot drawn on a gray square follows the mouse from left to right. Its movement is constrained to the middle third of the square.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   let x = constrain(mouseX, 33, 67);
     *   let y = 50;
     *
     *   strokeWeight(5);
     *   point(x, y);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('Two vertical lines. Two circles move horizontally with the mouse. One circle stops at the vertical lines.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Set boundaries and draw them.
     *   let leftWall = 25;
     *   let rightWall = 75;
     *   line(leftWall, 0, leftWall, 100);
     *   line(rightWall, 0, rightWall, 100);
     *
     *   // Draw a circle that follows the mouse freely.
     *   fill(255);
     *   circle(mouseX, 33, 9);
     *
     *   // Draw a circle that's constrained.
     *   let xc = constrain(mouseX, leftWall, rightWall);
     *   fill(0);
     *   circle(xc, 67, 9);
     * }
     */
    fn.constrain = function(n, low, high) {
      // p5._validateParameters('constrain', arguments);
      return Math.max(Math.min(n, high), low);
    };

    /**
     * Calculates the distance between two points.
     *
     * The version of `dist()` with four parameters calculates distance in two
     * dimensions.
     *
     * The version of `dist()` with six parameters calculates distance in three
     * dimensions.
     *
     * Use <a href="#/p5.Vector/dist">p5.Vector.dist()</a> to calculate the
     * distance between two <a href="#/p5.Vector">p5.Vector</a> objects.
     *
     * @method dist
     * @param  {Number} x1 x-coordinate of the first point.
     * @param  {Number} y1 y-coordinate of the first point.
     * @param  {Number} x2 x-coordinate of the second point.
     * @param  {Number} y2 y-coordinate of the second point.
     * @return {Number}    distance between the two points.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the coordinates.
     *   let x1 = 10;
     *   let y1 = 50;
     *   let x2 = 90;
     *   let y2 = 50;
     *
     *   // Draw the points and a line connecting them.
     *   line(x1, y1, x2, y2);
     *   strokeWeight(5);
     *   point(x1, y1);
     *   point(x2, y2);
     *
     *   // Calculate the distance.
     *   let d = dist(x1, y1, x2, y2);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the distance.
     *   text(d, 43, 40);
     *
     *   describe('Two dots connected by a horizontal line. The number 80 is written above the center of the line.');
     * }
     */
    /**
     * @method dist
     * @param  {Number} x1
     * @param  {Number} y1
     * @param  {Number} z1 z-coordinate of the first point.
     * @param  {Number} x2
     * @param  {Number} y2
     * @param  {Number} z2 z-coordinate of the second point.
     * @return {Number}    distance between the two points.
     */
    fn.dist = function(...args) {
      // p5._validateParameters('dist', args);
      if (args.length === 4) {
        //2D
        return Math.hypot(args[2] - args[0], args[3] - args[1]);
      } else if (args.length === 6) {
        //3D
        return Math.hypot(
          args[3] - args[0], args[4] - args[1], args[5] - args[2]
        );
      }
    };

    /**
     * Calculates the value of Euler's number e (2.71828...) raised to the power
     * of a number.
     *
     * @method exp
     * @param  {Number} n exponent to raise.
     * @return {Number}   e^n
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Top-left.
     *   let d = exp(1);
     *   circle(10, 10, d);
     *
     *   // Left-center.
     *   d = exp(2);
     *   circle(20, 20, d);
     *
     *   // Right-center.
     *   d = exp(3);
     *   circle(40, 40, d);
     *
     *   // Bottom-right.
     *   d = exp(4);
     *   circle(80, 80, d);
     *
     *   describe('A series of circles that grow exponentially from top left to bottom right.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   describe('A series of black dots that grow exponentially from left to right.');
     * }
     *
     * function draw() {
     *   // Invert the y-axis.
     *   scale(1, -1);
     *   translate(0, -100);
     *
     *   // Calculate the coordinates.
     *   let x = frameCount;
     *   let y = 0.005 * exp(x * 0.1);
     *
     *   // Draw a point.
     *   point(x, y);
     * }
     */
    fn.exp = Math.exp;

    /**
     * Calculates the closest integer value that is less than or equal to the
     * value of a number.
     *
     * @method floor
     * @param  {Number} n number to round down.
     * @return {Integer}  rounded down number.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Use RGB color with values from 0 to 1.
     *   colorMode(RGB, 1);
     *
     *   noStroke();
     *
     *   // Draw the left rectangle.
     *   let r = 0.8;
     *   fill(r, 0, 0);
     *   rect(0, 0, 50, 100);
     *
     *   // Round r down to 0.
     *   r = floor(r);
     *
     *   // Draw the right rectangle.
     *   fill(r, 0, 0);
     *   rect(50, 0, 50, 100);
     *
     *   describe('Two rectangles. The one on the left is bright red and the one on the right is black.');
     * }
     */
    fn.floor = Math.floor;

    /**
     * Calculates a number between two numbers at a specific increment.
     *
     * The `amt` parameter is the amount to interpolate between the two numbers.
     * 0.0 is equal to the first number, 0.1 is very near the first number, 0.5 is
     * half-way in between, and 1.0 is equal to the second number. The `lerp()`
     * function is convenient for creating motion along a straight path and for
     * drawing dotted lines.
     *
     * If the value of `amt` is less than 0 or more than 1, `lerp()` will return a
     * number outside of the original interval. For example, calling
     * `lerp(0, 10, 1.5)` will return 15.
     *
     * @method lerp
     * @param  {Number} start first value.
     * @param  {Number} stop  second value.
     * @param  {Number} amt   number.
     * @return {Number}       lerped value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Declare variables for coordinates.
     *   let a = 20;
     *   let b = 80;
     *   let c = lerp(a, b, 0.2);
     *   let d = lerp(a, b, 0.5);
     *   let e = lerp(a, b, 0.8);
     *
     *   strokeWeight(5);
     *
     *   // Draw the original points in black.
     *   stroke(0);
     *   point(a, 50);
     *   point(b, 50);
     *
     *   // Draw the lerped points in gray.
     *   stroke(100);
     *   point(c, 50);
     *   point(d, 50);
     *   point(e, 50);
     *
     *   describe('Five points in a horizontal line. The outer points are black and the inner points are gray.');
     * }
     *
     * @example
     * let x = 50;
     * let y = 50;
     * let targetX = 50;
     * let targetY = 50;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   describe('A white circle at the center of a gray canvas. The circle moves to where the user clicks, then moves smoothly back to the center.');
     * }
     *
     * function draw() {
     *   background(220);
     *
     *   // Move x and y toward the target.
     *   x = lerp(x, targetX, 0.05);
     *   y = lerp(y, targetY, 0.05);
     *
     *   // Draw the circle.
     *   circle(x, y, 20);
     * }
     *
     * // Set x and y when the user clicks the mouse.
     * function mouseClicked() {
     *   x = mouseX;
     *   y = mouseY;
     * }
     */
    fn.lerp = function(start, stop, amt) {
      // p5._validateParameters('lerp', arguments);
      return amt * (stop - start) + start;
    };

    /**
     * Calculates the natural logarithm (the base-e logarithm) of a number.
     *
     * `log()` expects the `n` parameter to be a value greater than 0 because
     * the natural logarithm is defined that way.
     *
     * @method log
     * @param  {Number} n number greater than 0.
     * @return {Number}   natural logarithm of n.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Top-left.
     *   let d = log(50);
     *   circle(33, 33, d);
     *
     *   // Bottom-right.
     *   d = log(500000000);
     *   circle(67, 67, d);
     *
     *   describe('Two white circles. The circle at the top-left is small. The circle at the bottom-right is about five times larger.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   describe('A series of black dots that get higher slowly from left to right.');
     * }
     *
     * function draw() {
     *   // Invert the y-axis.
     *   scale(1, -1);
     *   translate(0, -100);
     *
     *   // Calculate coordinates.
     *   let x = frameCount;
     *   let y = 15 * log(x);
     *
     *   // Draw a point.
     *   point(x, y);
     * }
     */
    fn.log = Math.log;

    /**
     * Calculates the magnitude, or length, of a vector.
     *
     * A vector can be thought of in different ways. In one view, a vector is a
     * point in space. The vector's components, `x` and `y`, are the point's
     * coordinates `(x, y)`. A vector's magnitude is the distance from the origin
     * `(0, 0)` to `(x, y)`. `mag(x, y)` is a shortcut for calling
     * `dist(0, 0, x, y)`.
     *
     * A vector can also be thought of as an arrow pointing in space. This view is
     * helpful for programming motion. See <a href="#/p5.Vector">p5.Vector</a> for
     * more details.
     *
     * Use <a href="#/p5.Vector/mag">p5.Vector.mag()</a> to calculate the
     * magnitude of a <a href="#/p5.Vector">p5.Vector</a> object.
     *
     * @method mag
     * @param  {Number} x first component.
     * @param  {Number} y second component.
     * @return {Number}   magnitude of vector.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the vector's components.
     *   let x = 30;
     *   let y = 40;
     *
     *   // Calculate the magnitude.
     *   let m = mag(x, y);
     *
     *   // Style the text.
     *   textSize(16);
     *
     *   // Display the vector and its magnitude.
     *   line(0, 0, x, y);
     *   text(m, x, y);
     *
     *   describe('A diagonal line is drawn from the top left of the canvas. The number 50 is written at the end of the line.');
     * }
     */
    fn.mag = function(x, y) {
      // p5._validateParameters('mag', arguments);
      return Math.hypot(x, y);
    };

    /**
     * Re-maps a number from one range to another.
     *
     * For example, calling `map(2, 0, 10, 0, 100)` returns 20. The first three
     * arguments set the original value to 2 and the original range from 0 to 10.
     * The last two arguments set the target range from 0 to 100. 20's position
     * in the target range [0, 100] is proportional to 2's position in the
     * original range [0, 10].
     *
     * The sixth parameter, `withinBounds`, is optional. By default, `map()` can
     * return values outside of the target range. For example,
     * `map(11, 0, 10, 0, 100)` returns 110. Passing `true` as the sixth parameter
     * constrains the remapped value to the target range. For example,
     * `map(11, 0, 10, 0, 100, true)` returns 100.
     *
     * @method map
     * @param  {Number} value  the value to be remapped.
     * @param  {Number} start1 lower bound of the value's current range.
     * @param  {Number} stop1  upper bound of the value's current range.
     * @param  {Number} start2 lower bound of the value's target range.
     * @param  {Number} stop2  upper bound of the value's target range.
     * @param  {Boolean} [withinBounds] constrain the value to the newly mapped range.
     * @return {Number}        remapped number.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('Two horizontal lines. The top line grows horizontally as the mouse moves to the right. The bottom line also grows horizontally but is scaled to stay on the left half of the canvas.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Draw the top line.
     *   line(0, 25, mouseX, 25);
     *
     *   // Remap mouseX from [0, 100] to [0, 50].
     *   let x = map(mouseX, 0, 100, 0, 50);
     *
     *   // Draw the bottom line.
     *   line(0, 75, 0, x);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A circle changes color from black to white as the mouse moves from left to right.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Remap mouseX from [0, 100] to [0, 255]
     *   let c = map(mouseX, 0, 100, 0, 255);
     *
     *   // Style the circle.
     *   fill(c);
     *
     *   // Draw the circle.
     *   circle(50, 50, 20);
     * }
     */
    fn.map = function(n, start1, stop1, start2, stop2, withinBounds) {
      // p5._validateParameters('map', arguments);
      const newval = (n - start1) / (stop1 - start1) * (stop2 - start2) + start2;
      if (!withinBounds) {
        return newval;
      }
      if (start2 < stop2) {
        return this.constrain(newval, start2, stop2);
      } else {
        return this.constrain(newval, stop2, start2);
      }
    };

    /**
     * Returns the largest value in a sequence of numbers.
     *
     * The version of `max()` with one parameter interprets it as an array of
     * numbers and returns the largest number.
     *
     * The version of `max()` with two or more parameters interprets them as
     * individual numbers and returns the largest number.
     *
     * @method max
     * @param  {Number} n0 first number to compare.
     * @param  {Number} n1 second number to compare.
     * @return {Number}             maximum number.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Calculate the maximum of 10, 5, and 20.
     *   let m = max(10, 5, 20);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the max.
     *   text(m, 50, 50);
     *
     *   describe('The number 20 written in the middle of a gray square.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an array of numbers.
     *   let numbers = [10, 5, 20];
     *
     *   // Calculate the maximum of the array.
     *   let m = max(numbers);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the max.
     *   text(m, 50, 50);
     *
     *   describe('The number 20 written in the middle of a gray square.');
     * }
     */
    /**
     * @method max
     * @param  {Number[]} nums numbers to compare.
     * @return {Number}
     */
    fn.max = function(...args) {
      const findMax = arr => {
        let max = -Infinity;
        for (let x of arr) {
          max = Math.max(max, x);
        }
        return max;
      };

      if (args[0] instanceof Array) {
        return findMax(args[0]);
      } else {
        return findMax(args);
      }
    };

    /**
     * Returns the smallest value in a sequence of numbers.
     *
     * The version of `min()` with one parameter interprets it as an array of
     * numbers and returns the smallest number.
     *
     * The version of `min()` with two or more parameters interprets them as
     * individual numbers and returns the smallest number.
     *
     * @method min
     * @param  {Number} n0 first number to compare.
     * @param  {Number} n1 second number to compare.
     * @return {Number}             minimum number.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Calculate the minimum of 10, 5, and 20.
     *   let m = min(10, 5, 20);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the min.
     *   text(m, 50, 50);
     *
     *   describe('The number 5 written in the middle of a gray square.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an array of numbers.
     *   let numbers = [10, 5, 20];
     *
     *   // Calculate the minimum of the array.
     *   let m = min(numbers);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the min.
     *   text(m, 50, 50);
     *
     *   describe('The number 5 written in the middle of a gray square.');
     * }
     */
    /**
     * @method min
     * @param  {Number[]} nums numbers to compare.
     * @return {Number}
     */
    fn.min = function(...args) {
      const findMin = arr => {
        let min = Infinity;
        for (let x of arr) {
          min = Math.min(min, x);
        }
        return min;
      };

      if (args[0] instanceof Array) {
        return findMin(args[0]);
      } else {
        return findMin(args);
      }
    };

    /**
     * Maps a number from one range to a value between 0 and 1.
     *
     * For example, `norm(2, 0, 10)` returns 0.2. 2's position in the original
     * range [0, 10] is proportional to 0.2's position in the range [0, 1]. This
     * is the same as calling `map(2, 0, 10, 0, 1)`.
     *
     * Numbers outside of the original range are not constrained between 0 and 1.
     * Out-of-range values are often intentional and useful.
     *
     * @method norm
     * @param  {Number} value incoming value to be normalized.
     * @param  {Number} start lower bound of the value's current range.
     * @param  {Number} stop  upper bound of the value's current range.
     * @return {Number}       normalized number.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Use RGB color with values from 0 to 1.
     *   colorMode(RGB, 1);
     *
     *   describe('A square changes color from black to red as the mouse moves from left to right.');
     * }
     *
     * function draw() {
     *   // Calculate the redValue.
     *   let redValue = norm(mouseX, 0, 100);
     *
     *   // Paint the background.
     *   background(redValue, 0, 0);
     * }
     */
    fn.norm = function(n, start, stop) {
      // p5._validateParameters('norm', arguments);
      return this.map(n, start, stop, 0, 1);
    };

    /**
     * Calculates exponential expressions such as <var>2<sup>3</sup></var>.
     *
     * For example, `pow(2, 3)` evaluates the expression
     * 2 &times; 2 &times; 2. `pow(2, -3)` evaluates 1 &#247;
     * (2 &times; 2 &times; 2).
     *
     * @method pow
     * @param  {Number} n base of the exponential expression.
     * @param  {Number} e power by which to raise the base.
     * @return {Number}   n^e.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the base of the exponent.
     *   let base = 3;
     *
     *   // Top-left.
     *   let d = pow(base, 1);
     *   circle(10, 10, d);
     *
     *   // Left-center.
     *   d = pow(base, 2);
     *   circle(20, 20, d);
     *
     *   // Right-center.
     *   d = pow(base, 3);
     *   circle(40, 40, d);
     *
     *   // Bottom-right.
     *   d = pow(base, 4);
     *   circle(80, 80, d);
     *
     *   describe('A series of circles that grow exponentially from top left to bottom right.');
     * }
     */
    fn.pow = Math.pow;

    /**
     * Calculates the integer closest to a number.
     *
     * For example, `round(133.8)` returns the value 134.
     *
     * The second parameter, `decimals`, is optional. It sets the number of
     * decimal places to use when rounding. For example, `round(12.34, 1)` returns
     * 12.3. `decimals` is 0 by default.
     *
     * @method round
     * @param  {Number} n number to round.
     * @param  {Number} [decimals] number of decimal places to round to, default is 0.
     * @return {Integer}  rounded number.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Round a number.
     *   let x = round(4.2);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the rounded number.
     *   text(x, 50, 50);
     *
     *   describe('The number 4 written in middle of the canvas.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Round a number to 2 decimal places.
     *   let x = round(12.782383, 2);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Display the rounded number.
     *   text(x, 50, 50);
     *
     *   describe('The number 12.78 written in middle of canvas.');
     * }
     */
    fn.round = function(n, decimals) {
      if (!decimals) {
        return Math.round(n);
      }
      const multiplier = Math.pow(10, decimals);
      return Math.round(n * multiplier) / multiplier;
    };

    /**
     * Calculates the square of a number.
     *
     * Squaring a number means multiplying the number by itself. For example,
     * `sq(3)` evaluates 3 &times; 3 which is 9. `sq(-3)` evaluates -3 &times; -3
     * which is also 9. Multiplying two negative numbers produces a positive
     * number. The value returned by `sq()` is always positive.
     *
     * @method sq
     * @param  {Number} n number to square.
     * @return {Number}   squared number.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Top-left.
     *   let d = sq(3);
     *   circle(33, 33, d);
     *
     *   // Bottom-right.
     *   d = sq(6);
     *   circle(67, 67, d);
     *
     *   describe('Two white circles. The circle at the top-left is small. The circle at the bottom-right is four times larger.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   describe('A series of black dots that get higher quickly from left to right.');
     * }
     *
     * function draw() {
     *   // Invert the y-axis.
     *   scale(1, -1);
     *   translate(0, -100);
     *
     *   // Calculate the coordinates.
     *   let x = frameCount;
     *   let y = 0.01 * sq(x);
     *
     *   // Draw the point.
     *   point(x, y);
     * }
     */
    fn.sq = n => n * n;

    /**
     * Calculates the square root of a number.
     *
     * A number's square root can be multiplied by itself to produce the original
     * number. For example, `sqrt(9)` returns 3 because 3 &times; 3 = 9. `sqrt()`
     * always returns a positive value. `sqrt()` doesn't work with negative arguments
     * such as `sqrt(-9)`.
     *
     * @method sqrt
     * @param  {Number} n non-negative number to square root.
     * @return {Number}   square root of number.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Top-left.
     *   let d = sqrt(16);
     *   circle(33, 33, d);
     *
     *   // Bottom-right.
     *   d = sqrt(1600);
     *   circle(67, 67, d);
     *
     *   describe('Two white circles. The circle at the top-left is small. The circle at the bottom-right is ten times larger.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   describe('A series of black dots that get higher slowly from left to right.');
     * }
     *
     * function draw() {
     *   // Invert the y-axis.
     *   scale(1, -1);
     *   translate(0, -100);
     *
     *   // Calculate the coordinates.
     *   let x = frameCount;
     *   let y = 5 * sqrt(x);
     *
     *   // Draw the point.
     *   point(x, y);
     * }
     */
    fn.sqrt = Math.sqrt;

    /**
     * Calculates the fractional part of a number.
     *
     * A number's fractional part includes its decimal values. For example,
     * `fract(12.34)` returns 0.34.
     *
     * @method fract
     * @param {Number} n number whose fractional part will be found.
     * @returns {Number} fractional part of n.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(16);
     *
     *   // Original number.
     *   let n = 56.78;
     *   text(n, 50, 33);
     *
     *   // Fractional part.
     *   let f = fract(n);
     *   text(f, 50, 67);
     *
     *   describe('The number 56.78 written above the number 0.78.');
     * }
     */
    fn.fract = function(toConvert) {
      // p5._validateParameters('fract', arguments);
      let sign = 0;
      let num = Number(toConvert);
      if (isNaN(num) || Math.abs(num) === Infinity) {
        return num;
      } else if (num < 0) {
        num = -num;
        sign = 1;
      }
      if (String(num).includes('.') && !String(num).includes('e')) {
        let toFract = String(num);
        toFract = Number('0' + toFract.slice(toFract.indexOf('.')));
        return Math.abs(sign - toFract);
      } else if (num < 1) {
        return Math.abs(sign - num);
      } else {
        return 0;
      }
    };
  }

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

  //////////////////////////////////////////////////////////////

  // http://mrl.nyu.edu/~perlin/noise/
  // Adapting from PApplet.java
  // which was adapted from toxi
  // which was adapted from the german demo group farbrausch
  // as used in their demo "art": http://www.farb-rausch.de/fr010src.zip

  // someday we might consider using "improved noise"
  // http://mrl.nyu.edu/~perlin/paper445.pdf
  // See: https://github.com/shiffman/The-Nature-of-Code-Examples-p5.js/
  //      blob/main/introduction/Noise1D/noise.js

  /**
   * @module Math
   * @submodule Noise
   * @for p5
   * @requires core
   */
  function noise(p5, fn){
    const PERLIN_YWRAPB = 4;
    const PERLIN_YWRAP = 1 << PERLIN_YWRAPB;
    const PERLIN_ZWRAPB = 8;
    const PERLIN_ZWRAP = 1 << PERLIN_ZWRAPB;
    const PERLIN_SIZE = 4095;

    let perlin_octaves = 4; // default to medium smooth
    let perlin_amp_falloff = 0.5; // 50% reduction/octave

    const scaled_cosine = i => 0.5 * (1.0 - Math.cos(i * Math.PI));

    let perlin; // will be initialized lazily by noise() or noiseSeed()

    /**
     * Returns random numbers that can be tuned to feel organic.
     *
     * Values returned by <a href="#/p5/random">random()</a> and
     * <a href="#/p5/randomGaussian">randomGaussian()</a> can change by large
     * amounts between function calls. By contrast, values returned by `noise()`
     * can be made "smooth". Calls to `noise()` with similar inputs will produce
     * similar outputs. `noise()` is used to create textures, motion, shapes,
     * terrains, and so on. Ken Perlin invented `noise()` while animating the
     * original <em>Tron</em> film in the 1980s.
     *
     * `noise()` always returns values between 0 and 1. It returns the same value
     * for a given input while a sketch is running. `noise()` produces different
     * results each time a sketch runs. The
     * <a href="#/p5/noiseSeed">noiseSeed()</a> function can be used to generate
     * the same sequence of Perlin noise values each time a sketch runs.
     *
     * The character of the noise can be adjusted in two ways. The first way is to
     * scale the inputs. `noise()` interprets inputs as coordinates. The sequence
     * of noise values will be smoother when the input coordinates are closer. The
     * second way is to use the <a href="#/p5/noiseDetail">noiseDetail()</a>
     * function.
     *
     * The version of `noise()` with one parameter computes noise values in one
     * dimension. This dimension can be thought of as space, as in `noise(x)`, or
     * time, as in `noise(t)`.
     *
     * The version of `noise()` with two parameters computes noise values in two
     * dimensions. These dimensions can be thought of as space, as in
     * `noise(x, y)`, or space and time, as in `noise(x, t)`.
     *
     * The version of `noise()` with three parameters computes noise values in
     * three dimensions. These dimensions can be thought of as space, as in
     * `noise(x, y, z)`, or space and time, as in `noise(x, y, t)`.
     *
     * @method noise
     * @param  {Number} x   x-coordinate in noise space.
     * @param  {Number} [y] y-coordinate in noise space.
     * @param  {Number} [z] z-coordinate in noise space.
     * @return {Number}     Perlin noise value at specified coordinates.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A black dot moves randomly on a gray square.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Calculate the coordinates.
     *   let x = 100 * noise(0.005 * frameCount);
     *   let y = 100 * noise(0.005 * frameCount + 10000);
     *
     *   // Draw the point.
     *   strokeWeight(5);
     *   point(x, y);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A black dot moves randomly on a gray square.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Set the noise level and scale.
     *   let noiseLevel = 100;
     *   let noiseScale = 0.005;
     *
     *   // Scale the input coordinate.
     *   let nt = noiseScale * frameCount;
     *
     *   // Compute the noise values.
     *   let x = noiseLevel * noise(nt);
     *   let y = noiseLevel * noise(nt + 10000);
     *
     *   // Draw the point.
     *   strokeWeight(5);
     *   point(x, y);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A hilly terrain drawn in gray against a black sky.');
     * }
     *
     * function draw() {
     *   // Set the noise level and scale.
     *   let noiseLevel = 100;
     *   let noiseScale = 0.02;
     *
     *   // Scale the input coordinate.
     *   let x = frameCount;
     *   let nx = noiseScale * x;
     *
     *   // Compute the noise value.
     *   let y = noiseLevel * noise(nx);
     *
     *   // Draw the line.
     *   line(x, 0, x, y);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A calm sea drawn in gray against a black sky.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Set the noise level and scale.
     *   let noiseLevel = 100;
     *   let noiseScale = 0.002;
     *
     *   // Iterate from left to right.
     *   for (let x = 0; x < 100; x += 1) {
     *     // Scale the input coordinates.
     *     let nx = noiseScale * x;
     *     let nt = noiseScale * frameCount;
     *
     *     // Compute the noise value.
     *     let y = noiseLevel * noise(nx, nt);
     *
     *     // Draw the line.
     *     line(x, 0, x, y);
     *   }
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the noise level and scale.
     *   let noiseLevel = 255;
     *   let noiseScale = 0.01;
     *
     *   // Iterate from top to bottom.
     *   for (let y = 0; y < 100; y += 1) {
     *     // Iterate from left to right.
     *     for (let x = 0; x < 100; x += 1) {
     *       // Scale the input coordinates.
     *       let nx = noiseScale * x;
     *       let ny = noiseScale * y;
     *
     *       // Compute the noise value.
     *       let c = noiseLevel * noise(nx, ny);
     *
     *       // Draw the point.
     *       stroke(c);
     *       point(x, y);
     *     }
     *   }
     *
     *   describe('A gray cloudy pattern.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A gray cloudy pattern that changes.');
     * }
     *
     * function draw() {
     *   // Set the noise level and scale.
     *   let noiseLevel = 255;
     *   let noiseScale = 0.009;
     *
     *   // Iterate from top to bottom.
     *   for (let y = 0; y < 100; y += 1) {
     *     // Iterate from left to right.
     *     for (let x = 0; x < width; x += 1) {
     *       // Scale the input coordinates.
     *       let nx = noiseScale * x;
     *       let ny = noiseScale * y;
     *       let nt = noiseScale * frameCount;
     *
     *       // Compute the noise value.
     *       let c = noiseLevel * noise(nx, ny, nt);
     *
     *       // Draw the point.
     *       stroke(c);
     *       point(x, y);
     *     }
     *   }
     * }
     */
    fn.noise = function(x, y = 0, z = 0) {
      if (perlin == null) {
        perlin = new Array(PERLIN_SIZE + 1);
        for (let i = 0; i < PERLIN_SIZE + 1; i++) {
          perlin[i] = Math.random();
        }
      }

      if (x < 0) {
        x = -x;
      }
      if (y < 0) {
        y = -y;
      }
      if (z < 0) {
        z = -z;
      }

      let xi = Math.floor(x),
        yi = Math.floor(y),
        zi = Math.floor(z);
      let xf = x - xi;
      let yf = y - yi;
      let zf = z - zi;
      let rxf, ryf;
      let r = 0;
      let ampl = 0.5;
      let n1, n2, n3;

      for (let o = 0; o < perlin_octaves; o++) {
        let of = xi + (yi << PERLIN_YWRAPB) + (zi << PERLIN_ZWRAPB);

        rxf = scaled_cosine(xf);
        ryf = scaled_cosine(yf);

        n1 = perlin[of & PERLIN_SIZE];
        n1 += rxf * (perlin[(of + 1) & PERLIN_SIZE] - n1);
        n2 = perlin[(of + PERLIN_YWRAP) & PERLIN_SIZE];
        n2 += rxf * (perlin[(of + PERLIN_YWRAP + 1) & PERLIN_SIZE] - n2);
        n1 += ryf * (n2 - n1);

        of += PERLIN_ZWRAP;
        n2 = perlin[of & PERLIN_SIZE];
        n2 += rxf * (perlin[(of + 1) & PERLIN_SIZE] - n2);
        n3 = perlin[(of + PERLIN_YWRAP) & PERLIN_SIZE];
        n3 += rxf * (perlin[(of + PERLIN_YWRAP + 1) & PERLIN_SIZE] - n3);
        n2 += ryf * (n3 - n2);

        n1 += scaled_cosine(zf) * (n2 - n1);

        r += n1 * ampl;
        ampl *= perlin_amp_falloff;
        xi <<= 1;
        xf *= 2;
        yi <<= 1;
        yf *= 2;
        zi <<= 1;
        zf *= 2;

        if (xf >= 1.0) {
          xi++;
          xf--;
        }
        if (yf >= 1.0) {
          yi++;
          yf--;
        }
        if (zf >= 1.0) {
          zi++;
          zf--;
        }
      }
      return r;
    };

    /**
     * Adjusts the character of the noise produced by the
     * <a href="#/p5/noise">noise()</a> function.
     *
     * Perlin noise values are created by adding layers of noise together. The
     * noise layers, called octaves, are similar to harmonics in music. Lower
     * octaves contribute more to the output signal. They define the overall
     * intensity of the noise. Higher octaves create finer-grained details.
     *
     * By default, noise values are created by combining four octaves. Each higher
     * octave contributes half as much (50% less) compared to its predecessor.
     * `noiseDetail()` changes the number of octaves and the falloff amount. For
     * example, calling `noiseDetail(6, 0.25)` ensures that
     * <a href="#/p5/noise">noise()</a> will use six octaves. Each higher octave
     * will contribute 25% as much (75% less) compared to its predecessor. Falloff
     * values between 0 and 1 are valid. However, falloff values greater than 0.5
     * might result in noise values greater than 1.
     *
     * @method noiseDetail
     * @param {Number} lod number of octaves to be used by the noise.
     * @param {Number} [falloff=0.5] falloff factor for each octave.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Set the noise level and scale.
     *   let noiseLevel = 255;
     *   let noiseScale = 0.02;
     *
     *   // Iterate from top to bottom.
     *   for (let y = 0; y < 100; y += 1) {
     *     // Iterate from left to right.
     *     for (let x = 0; x < 50; x += 1) {
     *       // Scale the input coordinates.
     *       let nx = noiseScale * x;
     *       let ny = noiseScale * y;
     *
     *       // Compute the noise value with six octaves
     *       // and a low falloff factor.
     *       noiseDetail(6, 0.25);
     *       let c = noiseLevel * noise(nx, ny);
     *
     *       // Draw the left side.
     *       stroke(c);
     *       point(x, y);
     *
     *       // Compute the noise value with four octaves
     *       // and a high falloff factor.
     *       noiseDetail(4, 0.5);
     *       c = noiseLevel * noise(nx, ny);
     *
     *       // Draw the right side.
     *       stroke(c);
     *       point(x + 50, y);
     *     }
     *   }
     *
     *   describe('Two gray cloudy patterns. The pattern on the right is cloudier than the pattern on the left.');
     * }
     */
    fn.noiseDetail = function(lod, falloff=0.5) {
      if (lod > 0) {
        perlin_octaves = lod;
      }
      if (falloff > 0) {
        perlin_amp_falloff = falloff;
      }
    };

    /**
     * @private
     * Returns the current number of octaves used by noise().
     */
    fn._getNoiseOctaves = function() {
      return perlin_octaves;
    };

    /**
     * @private
     * Returns the current falloff factor used by noise().
     */
    fn._getNoiseAmpFalloff = function() {
      return perlin_amp_falloff;
    };

    /**
     * Sets the seed value for the <a href="#/p5/noise">noise()</a> function.
     *
     * By default, <a href="#/p5/noise">noise()</a> produces different results
     * each time a sketch is run. Calling `noiseSeed()` with a constant argument,
     * such as `noiseSeed(99)`, makes <a href="#/p5/noise">noise()</a> produce the
     * same results each time a sketch is run.
     *
     * @method noiseSeed
     * @param {Number} seed   seed value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Set the noise seed for consistent results.
     *   noiseSeed(99);
     *
     *   describe('A black rectangle that grows randomly, first to the right and then to the left.');
     * }
     *
     * function draw() {
     *   // Set the noise level and scale.
     *   let noiseLevel = 100;
     *   let noiseScale = 0.005;
     *
     *   // Scale the input coordinate.
     *   let nt = noiseScale * frameCount;
     *
     *   // Compute the noise value.
     *   let x = noiseLevel * noise(nt);
     *
     *   // Draw the line.
     *   line(x, 0, x, height);
     * }
     */
    fn.noiseSeed = function(seed) {
      // Linear Congruential Generator
      // Variant of a Lehman Generator
      const lcg = (() => {
        // Set to values from http://en.wikipedia.org/wiki/Numerical_Recipes
        // m is basically chosen to be large (as it is the max period)
        // and for its relationships to a and c
        const m = 4294967296;
        // a - 1 should be divisible by m's prime factors
        const a = 1664525;
        // c and m should be co-prime
        const c = 1013904223;
        let seed, z;
        return {
          setSeed(val) {
            // pick a random seed if val is undefined or null
            // the >>> 0 casts the seed to an unsigned 32-bit integer
            z = seed = (val == null ? Math.random() * m : val) >>> 0;
          },
          getSeed() {
            return seed;
          },
          rand() {
            // define the recurrence relationship
            z = (a * z + c) % m;
            // return a float in [0, 1)
            // if z = m then z / m = 0 therefore (z % m) / m < 1 always
            return z / m;
          }
        };
      })();

      lcg.setSeed(seed);
      perlin = new Array(PERLIN_SIZE + 1);
      for (let i = 0; i < PERLIN_SIZE + 1; i++) {
        perlin[i] = lcg.rand();
      }
    };
  }

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

  /**
   * @module Math
   * @submodule Random
   * @for p5
   * @requires core
   */

  function random(p5, fn){
    // variables used for random number generators
    const randomStateProp = '_lcg_random_state';
    // Set to values from http://en.wikipedia.org/wiki/Numerical_Recipes
    // m is basically chosen to be large (as it is the max period)
    // and for its relationships to a and c
    const m = 4294967296;
    // a - 1 should be divisible by m's prime factors
    const a = 1664525;
    // c and m should be co-prime
    const c = 1013904223;
    let y2 = 0;

    // Linear Congruential Generator that stores its state at instance[stateProperty]
    fn._lcg = function(stateProperty) {
      // define the recurrence relationship
      this[stateProperty] = (a * this[stateProperty] + c) % m;
      // return a float in [0, 1)
      // we've just used % m, so / m is always < 1
      return this[stateProperty] / m;
    };

    fn._lcgSetSeed = function(stateProperty, val) {
      // pick a random seed if val is undefined or null
      // the >>> 0 casts the seed to an unsigned 32-bit integer
      this[stateProperty] = (val == null ? Math.random() * m : val) >>> 0;
    };

    /**
     * Sets the seed value for the <a href="#/p5/random">random()</a> and
     * <a href="#/p5/randomGaussian">randomGaussian()</a> functions.
     *
     * By default, <a href="#/p5/random">random()</a> and
     * <a href="#/p5/randomGaussian">randomGaussian()</a> produce different
     * results each time a sketch is run. Calling `randomSeed()` with a constant
     * argument, such as `randomSeed(99)`, makes these functions produce the same
     * results each time a sketch is run.
     *
     * @method randomSeed
     * @param {Number} seed   seed value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get random coordinates.
     *   let x = random(0, 100);
     *   let y = random(0, 100);
     *
     *   // Draw the white circle.
     *   circle(x, y, 10);
     *
     *   // Set a random seed for consistency.
     *   randomSeed(99);
     *
     *   // Get random coordinates.
     *   x = random(0, 100);
     *   y = random(0, 100);
     *
     *   // Draw the black circle.
     *   fill(0);
     *   circle(x, y, 10);
     *
     *   describe('A white circle appears at a random position. A black circle appears at (27.4, 25.8).');
     * }
     */
    fn.randomSeed = function(seed) {
      this._lcgSetSeed(randomStateProp, seed);
      this._gaussian_previous = false;
    };

    /**
     * Returns a random number or a random element from an array.
     *
     * `random()` follows uniform distribution, which means that all outcomes are
     * equally likely. When `random()` is used to generate numbers, all
     * numbers in the output range are equally likely to be returned. When
     * `random()` is used to select elements from an array, all elements are
     * equally likely to be chosen.
     *
     * By default, `random()` produces different results each time a sketch runs.
     * The <a href="#/p5/randomSeed">randomSeed()</a> function can be used to
     * generate the same sequence of numbers or choices each time a sketch runs.
     *
     * The version of `random()` with no parameters returns a random number from 0
     * up to but not including 1.
     *
     * The version of `random()` with one parameter works one of two ways. If the
     * argument passed is a number, `random()` returns a random number from 0 up
     * to but not including the number. For example, calling `random(5)` returns
     * values between 0 and 5. If the argument passed is an array, `random()`
     * returns a random element from that array. For example, calling
     * `random(['🦁', '🐯', '🐻'])` returns either a lion, tiger, or bear emoji.
     *
     * The version of `random()` with two parameters returns a random number from
     * a given range. The arguments passed set the range's lower and upper bounds.
     * For example, calling `random(-5, 10.2)` returns values from -5 up to but
     * not including 10.2.
     *
     * @method random
     * @param  {Number} [min]   lower bound (inclusive).
     * @param  {Number} [max]   upper bound (exclusive).
     * @return {Number} random number.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get random coordinates between 0 and 100.
     *   let x = random(0, 100);
     *   let y = random(0, 100);
     *
     *   // Draw a point.
     *   strokeWeight(5);
     *   point(x, y);
     *
     *   describe('A black dot appears in a random position on a gray square.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get random coordinates between 0 and 100.
     *   let x = random(100);
     *   let y = random(100);
     *
     *   // Draw the point.
     *   strokeWeight(5);
     *   point(x, y);
     *
     *   describe('A black dot appears in a random position on a gray square.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an array of emoji strings.
     *   let animals = ['🦁', '🐯', '🐻'];
     *
     *   // Choose a random element from the array.
     *   let choice = random(animals);
     *
     *   // Style the text.
     *   textAlign(CENTER);
     *   textSize(20);
     *
     *   // Display the emoji.
     *   text(choice, 50, 50);
     *
     *   describe('An animal face is displayed at random. Either a lion, tiger, or bear.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Slow the frame rate.
     *   frameRate(5);
     *
     *   describe('A black dot moves around randomly on a gray square.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Get random coordinates between 0 and 100.
     *   let x = random(100);
     *   let y = random(100);
     *
     *   // Draw the point.
     *   strokeWeight(5);
     *   point(x, y);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Slow the frame rate.
     *   frameRate(5);
     *
     *   describe('A black dot moves around randomly in the middle of a gray square.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Get random coordinates between 45 and 55.
     *   let x = random(45, 55);
     *   let y = random(45, 55);
     *
     *   // Draw the point.
     *   strokeWeight(5);
     *   point(x, y);
     * }
     *
     * @example
     * let x = 50;
     * let y = 50;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   describe('A black dot moves around randomly leaving a trail.');
     * }
     *
     * function draw() {
     *   // Update x and y randomly.
     *   x += random(-1, 1);
     *   y += random(-1, 1);
     *
     *   // Draw the point.
     *   point(x, y);
     * }
     */
    /**
     * @method random
     * @param  {Array} choices   array to choose from.
     * @return {*} random element from the array.
     */
    fn.random = function(min, max) {
      // p5._validateParameters('random', arguments);
      let rand;

      if (this[randomStateProp] != null) {
        rand = this._lcg(randomStateProp);
      } else {
        rand = Math.random();
      }
      if (typeof min === 'undefined') {
        return rand;
      } else if (typeof max === 'undefined') {
        if (min instanceof Array) {
          return min[Math.floor(rand * min.length)];
        } else {
          return rand * min;
        }
      } else {
        if (min > max) {
          const tmp = min;
          min = max;
          max = tmp;
        }

        return rand * (max - min) + min;
      }
    };

    /**
     * Returns a random number fitting a Gaussian, or normal, distribution.
     *
     * Normal distributions look like bell curves when plotted. Values from a
     * normal distribution cluster around a central value called the mean. The
     * cluster's standard deviation describes its spread.
     *
     * By default, `randomGaussian()` produces different results each time a
     * sketch runs. The <a href="#/p5/randomSeed">randomSeed()</a> function can be
     * used to generate the same sequence of numbers each time a sketch runs.
     *
     * There's no minimum or maximum value that `randomGaussian()` might return.
     * Values far from the mean are very unlikely and values near the mean are
     * very likely.
     *
     * The version of `randomGaussian()` with no parameters returns values with a
     * mean of 0 and standard deviation of 1.
     *
     * The version of `randomGaussian()` with one parameter interprets the
     * argument passed as the mean. The standard deviation is 1.
     *
     * The version of `randomGaussian()` with two parameters interprets the first
     * argument passed as the mean and the second as the standard deviation.
     *
     * @method randomGaussian
     * @param  {Number} [mean]  mean.
     * @param  {Number} [sd]    standard deviation.
     * @return {Number} random number.
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   describe('Three horizontal black lines are filled in randomly. The top line spans entire canvas. The middle line is very short. The bottom line spans two-thirds of the canvas.');
     * }
     *
     * function draw() {
     *   // Style the circles.
     *   noStroke();
     *   fill(0, 10);
     *
     *   // Uniform distribution between 0 and 100.
     *   let x = random(100);
     *   let y = 25;
     *   circle(x, y, 5);
     *
     *   // Gaussian distribution with a mean of 50 and sd of 1.
     *   x = randomGaussian(50);
     *   y = 50;
     *   circle(x, y, 5);
     *
     *   // Gaussian distribution with a mean of 50 and sd of 10.
     *   x = randomGaussian(50, 10);
     *   y = 75;
     *   circle(x, y, 5);
     * }
     */
    fn.randomGaussian = function(mean, sd = 1) {
      let y1, x1, x2, w;
      if (this._gaussian_previous) {
        y1 = y2;
        this._gaussian_previous = false;
      } else {
        do {
          x1 = this.random(2) - 1;
          x2 = this.random(2) - 1;
          w = x1 * x1 + x2 * x2;
        } while (w >= 1);
        w = Math.sqrt(-2 * Math.log(w) / w);
        y1 = x1 * w;
        y2 = x2 * w;
        this._gaussian_previous = true;
      }

      const m = mean || 0;
      return y1 * sd + m;
    };
  }

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

  /**
   * @module Math
   * @for p5
   * @requires core
   */

  function math$1(p5, fn) {
    /**
     * Creates a new <a href="#/p5.Vector">p5.Vector</a> object.
     *
     * A vector can be thought of in different ways. In one view, a vector is like
     * an arrow pointing in space. Vectors have both magnitude (length) and
     * direction. This view is helpful for programming motion.
     *
     * A vector's components determine its magnitude and direction. For example,
     * calling `createVector(3, 4)` creates a new
     * <a href="#/p5.Vector">p5.Vector</a> object with an x-component of 3 and a
     * y-component of 4. From the origin, this vector's tip is 3 units to the
     * right and 4 units down.
     *
     * You can also pass N dimensions to the `createVector` function. For example,
     * calling `createVector(1, 2, 3, 4)` creates a vector with four components.
     * This allows for flexibility in representing vectors in higher-dimensional
     * spaces.
     *
     * Calling `createVector()` **with no arguments** is **deprecated** and will emit
     * a friendly warning. Use `createVector(0)`, `createVector(0, 0)`, or
     * `createVector(0, 0, 0)` instead.
     *
     * <a href="#/p5.Vector">p5.Vector</a> objects are often used to program
     * motion because they simplify the math. For example, a moving ball has a
     * position and a velocity. Position describes where the ball is in space. The
     * ball's position vector extends from the origin to the ball's center.
     * Velocity describes the ball's speed and the direction it's moving. If the
     * ball is moving straight up, its velocity vector points straight up. Adding
     * the ball's velocity vector to its position vector moves it, as in
     * `pos.add(vel)`. Vector math relies on methods inside the
     * <a href="#/p5.Vector">p5.Vector</a> class.
     *
     * @method createVector
     * @param {...Number} x Zero or more numbers, representing each component of the vector.
     * @return {p5.Vector} new <a href="#/p5.Vector">p5.Vector</a> object.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create p5.Vector objects.
     *   let p1 = createVector(25, 25);
     *   let p2 = createVector(50, 50);
     *   let p3 = createVector(75, 75);
     *
     *   // Draw the dots.
     *   strokeWeight(5);
     *   point(p1);
     *   point(p2);
     *   point(p3);
     *
     *   describe('Three black dots form a diagonal line from top left to bottom right.');
     * }
     *
     * @example
     * let pos;
     * let vel;
     *
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   // Create p5.Vector objects.
     *   pos = createVector(50, 100);
     *   vel = createVector(0, -1);
     *
     *   describe('A black dot moves from bottom to top on a gray square. The dot reappears at the bottom when it reaches the top.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Add velocity to position.
     *   pos.add(vel);
     *
     *   // If the dot reaches the top of the canvas,
     *   // restart from the bottom.
     *   if (pos.y < 0) {
     *     pos.y = 100;
     *   }
     *
     *   // Draw the dot.
     *   strokeWeight(5);
     *   point(pos);
     * }
     */
    fn.createVector = function (x, y, z) {
      if (arguments.length === 0) {
        p5._friendlyError(
          'In 1.x, createVector() was a shortcut for createVector(0, 0, 0). In 2.x, p5.js has vectors of any dimension, so you must provide your desired number of zeros. Use createVector(0, 0) for a 2D vector and createVector(0, 0, 0) for a 3D vector.'
        );
      }
      if (this instanceof p5) {
        return new p5.Vector(
          this._fromRadians.bind(this),
          this._toRadians.bind(this),
          ...arguments
        );
      } else {
        return new p5.Vector(x, y, z);
      }
    };

    /**
     * Creates a new <a href="#/p5.Matrix">p5.Matrix</a> object.
     *
     * A matrix is a mathematical concept that is useful in many fields, including
     * computer graphics. In p5.js, matrices are used to perform transformations
     * on shapes and images. The `createMatrix` method can take a column-major
     * array representation of a square matrix as an argument. In the current implementation we only use squared matrices.
     *
     * @private
     * @method createMatrix
     * @param {Array<Number>} components Column-major array representation of the square matrix.
     *
     * @return {p5.Matrix} new <a href="#/p5.Matrix">p5.Matrix</a> object.
     *
     * @example
     * // META:norender
     * function setup() {
     *   let matrix = createMatrix([1, 2, 3, 4, 5, 6, 7, 8, 9]);
     * }
     */
    fn.createMatrix = function (...args) {
      return new p5.Matrix(...args);
    };
  }

  if (typeof p5 !== 'undefined') {
    math$1(p5, p5.prototype);
  }

  function math(p5){
    p5.registerAddon(calculation);
    p5.registerAddon(noise);
    p5.registerAddon(random);
    p5.registerAddon(trigonometry);
    p5.registerAddon(math$1);
    p5.registerAddon(vector);
  }

  /**
   * @module Data
   * @submodule Conversion
   * @for p5
   * @requires core
   */

  function conversion(p5, fn){
    /**
     * Converts a `String` to a floating point (decimal) `Number`.
     *
     * `float()` converts strings that resemble numbers, such as `'12.34'`, into
     * numbers.
     *
     * The parameter, `str`, is the string value to convert. For example, calling
     * `float('12.34')` returns the number `12.34`.  If an array of strings is
     * passed, as in `float(['12.34', '56.78'])`, then an array of numbers will be
     * returned.
     *
     * Note: If a string can't be converted to a number, as in `float('giraffe')`,
     * then the value `NaN` (not a number) will be returned.
     *
     * @method float
     * @param {String}  str string to convert.
     * @return {Number} converted number.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a string variable.
     *   let original = '12.3';
     *
     *   // Convert the string to a number.
     *   let converted = float(original);
     *
     *   // Double the converted value.
     *   let twice = converted * 2;
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(12);
     *
     *   // Display the original and converted values.
     *   text(`${original} × 2 = ${twice}`, 50, 50);
     *
     *   describe('The text "12.3 × 2 = 24.6" written in black on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an array of strings.
     *   let original = ['60', '30', '15'];
     *
     *   // Convert the strings to numbers.
     *   let diameters = float(original);
     *
     *   for (let d of diameters) {
     *     // Draw a circle.
     *     circle(50, 50, d);
     *   }
     *
     *   describe('Three white, concentric circles on a gray background.');
     * }
     */
    /**
     * @method float
     * @param {String[]} ns array of strings to convert.
     * @return {Number[]} converted numbers.
     */
    fn.float = function(str) {
      if (str instanceof Array) {
        return str.map(parseFloat);
      }
      return parseFloat(str);
    };

    /**
     * Converts a `Boolean`, `String`, or decimal `Number` to an integer.
     *
     * `int()` converts values to integers. Integers are positive or negative
     * numbers without decimals. If the original value has decimals, as in -34.56,
     * they're removed to produce an integer such as -34.
     *
     * The parameter, `n`, is the value to convert. If `n` is a Boolean, as in
     * `int(false)` or `int(true)`, then the number 0 (`false`) or 1 (`true`) will
     * be returned. If `n` is a string or number, as in `int('45')` or
     * `int(67.89)`, then an integer will be returned. If an array is passed, as
     * in `int([12.34, 56.78])`, then an array of integers will be returned.
     *
     * Note: If a value can't be converted to a number, as in `int('giraffe')`,
     * then the value `NaN` (not a number) will be returned.
     *
     * @method int
     * @param {String|Boolean|Number} n value to convert.
     * @return {Number} converted number.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a Boolean variable.
     *   let original = false;
     *
     *   // Convert the Boolean to an integer.
     *   let converted = int(original);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(16);
     *
     *   // Display the original and converted values.
     *   text(`${original} : ${converted}`, 50, 50);
     *
     *   describe('The text "false : 0" written in black on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a string variable.
     *   let original = '12.34';
     *
     *   // Convert the string to an integer.
     *   let converted = int(original);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(14);
     *
     *   // Display the original and converted values.
     *   text(`${original} ≈ ${converted}`, 50, 50);
     *
     *   describe('The text "12.34 ≈ 12" written in black on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a decimal number variable.
     *   let original = 12.34;
     *
     *   // Convert the decimal number to an integer.
     *   let converted = int(original);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(14);
     *
     *   // Display the original and converted values.
     *   text(`${original} ≈ ${converted}`, 50, 50);
     *
     *   describe('The text "12.34 ≈ 12" written in black on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an array of strings.
     *   let original = ['60', '30', '15'];
     *
     *   // Convert the strings to integers.
     *   let diameters = int(original);
     *
     *   for (let d of diameters) {
     *     // Draw a circle.
     *     circle(50, 50, d);
     *   }
     *
     *   describe('Three white, concentric circles on a gray background.');
     * }
     */
    /**
     * @method int
     * @param {Array} ns values to convert.
     * @return {Number[]} converted numbers.
     */
    fn.int = function(n, radix = 10) {
      if (n === Infinity || n === 'Infinity') {
        return Infinity;
      } else if (n === -Infinity || n === '-Infinity') {
        return -Infinity;
      } else if (typeof n === 'string') {
        return parseInt(n, radix);
      } else if (typeof n === 'number') {
        return n | 0;
      } else if (typeof n === 'boolean') {
        return n ? 1 : 0;
      } else if (n instanceof Array) {
        return n.map(n => fn.int(n, radix));
      }
    };

    /**
     * Converts a `Boolean` or `Number` to `String`.
     *
     * `str()` converts values to strings. See the
     * <a href="#/p5/String">String</a> reference page for guidance on using
     * template literals instead.
     *
     * The parameter, `n`, is the value to convert. If `n` is a Boolean, as in
     * `str(false)` or `str(true)`, then the value will be returned as a string,
     * as in `'false'` or `'true'`. If `n` is a number, as in `str(123)`, then its
     * value will be returned as a string, as in `'123'`. If an array is passed,
     * as in `str([12.34, 56.78])`, then an array of strings will be returned.
     *
     * @method str
     * @param {String|Boolean|Number} n value to convert.
     * @return {String} converted string.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a Boolean variable.
     *   let original = false;
     *
     *   // Convert the Boolean to a string.
     *   let converted = str(original);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(16);
     *
     *   // Display the original and converted values.
     *   text(`${original} : ${converted}`, 50, 50);
     *
     *   describe('The text "false : false" written in black on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a number variable.
     *   let original = 123;
     *
     *   // Convert the number to a string.
     *   let converted = str(original);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(16);
     *
     *   // Display the original and converted values.
     *   text(`${original} = ${converted}`, 50, 50);
     *
     *   describe('The text "123 = 123" written in black on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an array of numbers.
     *   let original = [12, 34, 56];
     *
     *   // Convert the numbers to strings.
     *   let strings = str(original);
     *
     *   // Create an empty string variable.
     *   let final = '';
     *
     *   // Concatenate all the strings.
     *   for (let s of strings) {
     *     final += s;
     *   }
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(16);
     *
     *   // Display the concatenated string.
     *   text(final, 50, 50);
     *
     *   describe('The text "123456" written in black on a gray background.');
     * }
     */
    fn.str = function(n) {
      if (n instanceof Array) {
        return n.map(fn.str);
      } else {
        return String(n);
      }
    };

    /**
     * Converts a `String` or `Number` to a `Boolean`.
     *
     * `boolean()` converts values to `true` or `false`.
     *
     * The parameter, `n`, is the value to convert. If `n` is a string, then
     * `boolean('true')` will return `true` and every other string value will
     * return `false`. If `n` is a number, then `boolean(0)` will return `false`
     * and every other numeric value will return `true`. If an array is passed, as
     * `in boolean([0, 1, 'true', 'blue'])`, then an array of Boolean values will
     * be returned.
     *
     * @method boolean
     * @param {String|Boolean|Number} n value to convert.
     * @return {Boolean} converted Boolean value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a number variable.
     *   let original = 0;
     *
     *   // Convert the number to a Boolean value.
     *   let converted = boolean(original);
     *
     *   // Style the circle based on the converted value.
     *   if (converted === true) {
     *     fill('blue');
     *   } else {
     *     fill('red');
     *   }
     *
     *   // Draw the circle.
     *   circle(50, 50, 40);
     *
     *   describe('A red circle on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a string variable.
     *   let original = 'true';
     *
     *   // Convert the string to a Boolean value.
     *   let converted = boolean(original);
     *
     *   // Style the circle based on the converted value.
     *   if (converted === true) {
     *     fill('blue');
     *   } else {
     *     fill('red');
     *   }
     *
     *   // Draw the circle.
     *   circle(50, 50, 40);
     *
     *   describe('A blue circle on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an array of values.
     *   let original = [0, 'hi', 123, 'true'];
     *
     *   // Convert the array to a Boolean values.
     *   let converted = boolean(original);
     *
     *   // Iterate over the array of converted Boolean values.
     *   for (let i = 0; i < converted.length; i += 1) {
     *
     *     // Style the circle based on the converted value.
     *     if (converted[i] === true) {
     *       fill('blue');
     *     } else {
     *       fill('red');
     *     }
     *
     *     // Calculate the x-coordinate.
     *     let x = (i + 1) * 20;
     *
     *     // Draw the circle.
     *     circle(x, 50, 15);
     *   }
     *
     *   describe(
     *     'A row of circles on a gray background. The two circles on the left are red and the two on the right are blue.'
     *   );
     * }
     */
    /**
     * @method boolean
     * @param {Array} ns values to convert.
     * @return {Boolean[]} converted Boolean values.
     */
    fn.boolean = function(n) {
      if (typeof n === 'number') {
        return n !== 0;
      } else if (typeof n === 'string') {
        return n.toLowerCase() === 'true';
      } else if (typeof n === 'boolean') {
        return n;
      } else if (n instanceof Array) {
        return n.map(fn.boolean);
      }
    };

    /**
     * Converts a `Boolean`, `String`, or `Number` to its byte value.
     *
     * `byte()` converts a value to an integer (whole number) between -128 and
     * 127. Values greater than 127 wrap around while negative values are
     * unchanged. For example, 128 becomes -128 and -129 remains the same.
     *
     * The parameter, `n`, is the value to convert. If `n` is a Boolean, as in
     * `byte(false)` or `byte(true)`, the number 0 (`false`) or 1 (`true`) will be
     * returned. If `n` is a string or number, as in `byte('256')` or `byte(256)`,
     * then the byte value will be returned. Decimal values are ignored. If an
     * array is passed, as in `byte([true, 123, '456'])`, then an array of byte
     * values will be returned.
     *
     * Note: If a value can't be converted to a number, as in `byte('giraffe')`,
     * then the value `NaN` (not a number) will be returned.
     *
     * @method byte
     * @param {String|Boolean|Number} n value to convert.
     * @return {Number} converted byte value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a Boolean variable.
     *   let original = true;
     *
     *   // Convert the Boolean to its byte value.
     *   let converted = byte(original);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(16);
     *
     *   // Display the original and converted values.
     *   text(`${original} : ${converted}`, 50, 50);
     *
     *   describe('The text "true : 1" written in black on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a string variable.
     *   let original = '256';
     *
     *   // Convert the string to its byte value.
     *   let converted = byte(original);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(16);
     *
     *   // Display the original and converted values.
     *   text(`${original} : ${converted}`, 50, 50);
     *
     *   describe('The text "256 : 0" written in black on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a number variable.
     *   let original = 256;
     *
     *   // Convert the number to its byte value.
     *   let converted = byte(original);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(16);
     *
     *   // Display the original and converted values.
     *   text(`${original} : ${converted}`, 50, 50);
     *
     *   describe('The text "256 : 0" written in black on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an array of values.
     *   let original = [false, '64', 383];
     *
     *   // Convert the array elements to their byte values.
     *   let converted = byte(original);
     *
     *   // Iterate over the converted array elements.
     *   for (let i = 0; i < converted.length; i += 1) {
     *
     *     // Style the circle.
     *     fill(converted[i]);
     *
     *     // Calculate the x-coordinate.
     *     let x = (i + 1) * 25;
     *
     *     // Draw the circle.
     *     circle(x, 50, 20);
     *   }
     *
     *   describe(
     *     'Three gray circles on a gray background. The circles get lighter from left to right.'
     *   );
     * }
     */
    /**
     * @method byte
     * @param {Array} ns values to convert.
     * @return {Number[]} converted byte values.
     */
    fn.byte = function(n) {
      const nn = fn.int(n, 10);
      if (typeof nn === 'number') {
        return (nn + 128) % 256 - 128;
      } else if (nn instanceof Array) {
        return nn.map(fn.byte);
      }
    };

    /**
     * Converts a `Number` or `String` to a single-character `String`.
     *
     * `char()` converts numbers to their single-character string representations.
     *
     * The parameter, `n`, is the value to convert. If a number is passed, as in
     * `char(65)`, the corresponding single-character string is returned. If a
     * string is passed, as in `char('65')`, the string is converted to an integer
     * (whole number) and the corresponding single-character string is returned.
     * If an array is passed, as in `char([65, 66, 67])`, an array of
     * single-character strings is returned.
     *
     * See <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCharCode" target="_blank">MDN</a>
     * for more information about conversions.
     *
     * @method char
     * @param {String|Number} n value to convert.
     * @return {String} converted single-character string.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a number variable.
     *   let original = 65;
     *
     *   // Convert the number to a char.
     *   let converted = char(original);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(16);
     *
     *   // Display the original and converted values.
     *   text(`${original} : ${converted}`, 50, 50);
     *
     *   describe('The text "65 : A" written in black on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a string variable.
     *   let original = '65';
     *
     *   // Convert the string to a char.
     *   let converted = char(original);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(16);
     *
     *   // Display the original and converted values.
     *   text(`${original} : ${converted}`, 50, 50);
     *
     *   describe('The text "65 : A" written in black on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an array of numbers.
     *   let original = ['65', 66, '67'];
     *
     *   // Convert the string to a char.
     *   let converted = char(original);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(16);
     *
     *   // Iterate over elements of the converted array.
     *   for (let i = 0; i < converted.length; i += 1) {
     *
     *     // Calculate the y-coordinate.
     *     let y = (i + 1) * 25;
     *
     *     // Display the original and converted values.
     *     text(`${original[i]} : ${converted[i]}`, 50, y);
     *   }
     *
     *   describe(
     *     'The text "65 : A", "66 : B", and "67 : C" written on three separate lines. The text is in black on a gray background.'
     *   );
     * }
     */
    /**
     * @method char
     * @param {Array} ns values to convert.
     * @return {String[]} converted single-character strings.
     */
    fn.char = function(n) {
      if (typeof n === 'number' && !isNaN(n)) {
        return String.fromCharCode(n);
      } else if (n instanceof Array) {
        return n.map(fn.char);
      } else if (typeof n === 'string') {
        return fn.char(parseInt(n, 10));
      }
    };

    /**
     * Converts a single-character `String` to a `Number`.
     *
     * `unchar()` converts single-character strings to their corresponding
     * integer (whole number).
     *
     * The parameter, `n`, is the character to convert. For example,
     * `unchar('A')`, returns the number 65. If an array is passed, as in
     * `unchar(['A', 'B', 'C'])`, an array of integers is returned.
     *
     * @method unchar
     * @param {String} n value to convert.
     * @return {Number} converted number.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a string variable.
     *   let original = 'A';
     *
     *   // Convert the string to a number.
     *   let converted = unchar(original);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(16);
     *
     *   // Display the original and converted values.
     *   text(`${original} : ${converted}`, 50, 50);
     *
     *   describe('The text "A : 65" written in black on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an array of characters.
     *   let original = ['A', 'B', 'C'];
     *
     *   // Convert the string to a number.
     *   let converted = unchar(original);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(16);
     *
     *   // Iterate over elements of the converted array.
     *   for (let i = 0; i < converted.length; i += 1) {
     *
     *     // Calculate the y-coordinate.
     *     let y = (i + 1) * 25;
     *
     *     // Display the original and converted values.
     *     text(`${original[i]} : ${converted[i]}`, 50, y);
     *   }
     *
     *   describe(
     *     'The text "A : 65", "B : 66", and "C :67" written on three separate lines. The text is in black on a gray background.'
     *   );
     * }
     */
    /**
     * @method unchar
     * @param {String[]} ns values to convert.
     * @return {Number[]} converted numbers.
     */
    fn.unchar = function(n) {
      if (typeof n === 'string' && n.length === 1) {
        return n.charCodeAt(0);
      } else if (n instanceof Array) {
        return n.map(fn.unchar);
      }
    };

    /**
     * Converts a `Number` to a `String` with its hexadecimal value.
     *
     * `hex()` converts a number to a string with its hexadecimal number value.
     * Hexadecimal (hex) numbers are base-16, which means there are 16 unique
     * digits. Hex extends the numbers 0–9 with the letters A–F. For example, the
     * number `11` (eleven) in base-10 is written as the letter `B` in hex.
     *
     * The first parameter, `n`, is the number to convert. For example, `hex(20)`,
     * returns the string `'00000014'`. If an array is passed, as in
     * `hex([1, 10, 100])`, an array of hexadecimal strings is returned.
     *
     * The second parameter, `digits`, is optional. If a number is passed, as in
     * `hex(20, 2)`, it sets the number of hexadecimal digits to display. For
     * example, calling `hex(20, 2)` returns the string `'14'`.
     *
     * @method hex
     * @param {Number} n value to convert.
     * @param {Number} [digits] number of digits to include.
     * @return {String} converted hexadecimal value.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a number variable.
     *   let original = 20;
     *
     *   // Convert the number to a hex string.
     *   let converted = hex(original);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(14);
     *
     *   // Display the original and converted values.
     *   text(`${original} = ${converted}`, 50, 50);
     *
     *   describe('The text "20 = 00000014" written in black on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a number variable.
     *   let original = 20;
     *
     *   // Convert the number to a hex string.
     *   // Only display two hex digits.
     *   let converted = hex(original, 2);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(16);
     *
     *   // Display the original and converted values.
     *   text(`${original} = ${converted}`, 50, 50);
     *
     *   describe('The text "20 = 14" written in black on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an array of numbers.
     *   let original = [1, 10, 100];
     *
     *   // Convert the numbers to hex strings.
     *   // Only use two hex digits.
     *   let converted = hex(original, 2);
     *
     *   // Style the text.
     *   textAlign(RIGHT, CENTER);
     *   textSize(16);
     *
     *   // Iterate over the converted values.
     *   for (let i = 0; i < converted.length; i += 1) {
     *
     *     // Calculate the y-coordinate.
     *     let y = (i + 1) * 25;
     *
     *     // Display the original and converted values.
     *     text(`${ original[i]} = ${converted[i]}`, 75, y);
     *   }
     *
     *   describe(
     *     'The text "1 = 01", "10 = 0A", and "100 = 64" written on three separate lines. The text is in black on a gray background.'
     *   );
     * }
     */
    /**
     * @method hex
     * @param {Number[]} ns values to convert.
     * @param {Number} [digits]
     * @return {String[]} converted hexadecimal values.
     */
    fn.hex = function(n, digits) {
      digits = digits === undefined || digits === null ? (digits = 8) : digits;
      if (n instanceof Array) {
        return n.map(n => fn.hex(n, digits));
      } else if (n === Infinity || n === -Infinity) {
        const c = n === Infinity ? 'F' : '0';
        return c.repeat(digits);
      } else if (typeof n === 'number') {
        if (n < 0) {
          n = 0xffffffff + n + 1;
        }
        let hex = Number(n)
          .toString(16)
          .toUpperCase();
        while (hex.length < digits) {
          hex = `0${hex}`;
        }
        if (hex.length >= digits) {
          hex = hex.substring(hex.length - digits, hex.length);
        }
        return hex;
      }
    };

    /**
     * Converts a `String` with a hexadecimal value to a  `Number`.
     *
     * `unhex()` converts a string with its hexadecimal number value to a number.
     * Hexadecimal (hex) numbers are base-16, which means there are 16 unique
     * digits. Hex extends the numbers 0–9 with the letters A–F. For example, the
     * number `11` (eleven) in base-10 is written as the letter `B` in hex.
     *
     * The first parameter, `n`, is the hex string to convert. For example,
     * `unhex('FF')`, returns the number 255. If an array is passed, as in
     * `unhex(['00', '80', 'FF'])`, an array of numbers is returned.
     *
     * @method unhex
     * @param {String} n value to convert.
     * @return {Number} converted number.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a a hex string variable
     *   let original = 'FF';
     *
     *   // Convert the hex string to a number.
     *   let converted = unhex(original);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(16);
     *
     *   // Display the original and converted values.
     *   text(`${original} = ${converted}`, 50, 50);
     *
     *   describe('The text "FF = 255" written in black on a gray background.');
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an array of numbers.
     *   let original = ['00', '80', 'FF'];
     *
     *   // Convert the numbers to hex strings.
     *   // Only use two hex digits.
     *   let converted = unhex(original, 2);
     *
     *   // Style the text.
     *   textAlign(RIGHT, CENTER);
     *   textSize(16);
     *
     *   // Iterate over the converted values.
     *   for (let i = 0; i < converted.length; i += 1) {
     *
     *     // Calculate the y-coordinate.
     *     let y = (i + 1) * 25;
     *
     *     // Display the original and converted values.
     *     text(`${ original[i]} = ${converted[i]}`, 80, y);
     *   }
     *
     *   describe(
     *     'The text "00 = 0", "80 = 128", and "FF = 255" written on three separate lines. The text is in black on a gray background.'
     *   );
     * }
     */
    /**
     * @method unhex
     * @param {String[]} ns values to convert.
     * @return {Number[]} converted numbers.
     */
    fn.unhex = function(n) {
      if (n instanceof Array) {
        return n.map(fn.unhex);
      } else {
        return parseInt(`0x${n}`, 16);
      }
    };
  }

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

  /**
   * @module Data
   * @submodule Utility Functions
   * @for p5
   * @requires core
   */

  function utilityFunctions(p5, fn){
    /**
     * Converts a `Number` into a `String` with a given number of digits.
     *
     * `nf()` converts numbers such as `123.45` into strings formatted with a set
     * number of digits, as in `'123.4500'`.
     *
     * The first parameter, `num`, is the number to convert to a string. For
     * example, calling `nf(123.45)` returns the string `'123.45'`. If an array of
     * numbers is passed, as in `nf([123.45, 67.89])`, an array of formatted
     * strings will be returned.
     *
     * The second parameter, `left`, is optional. If a number is passed, as in
     * `nf(123.45, 4)`, it sets the minimum number of digits to include to the
     * left of the decimal place. If `left` is larger than the number of digits in
     * `num`, then unused digits will be set to 0. For example, calling
     * `nf(123.45, 4)` returns the string `'0123.45'`.
     *
     * The third parameter, `right`, is also optional. If a number is passed, as
     * in `nf(123.45, 4, 1)`, it sets the minimum number of digits to include to
     * the right of the decimal place. If `right` is smaller than the number of
     * decimal places in `num`, then `num` will be rounded to the given number of
     * decimal places. For example, calling `nf(123.45, 4, 1)` returns the string
     * `'0123.5'`. If right is larger than the number of decimal places in `num`,
     * then unused decimal places will be set to 0. For example, calling
     * `nf(123.45, 4, 3)` returns the string `'0123.450'`.
     *
     * When the number is negative, for example, calling `nf(-123.45, 5, 2)`
     * returns the string `'-00123.45'`.
     *
     * @method nf
     * @param {Number|String} num number to format.
     * @param {Integer|String} [left] number of digits to include to the left of
     *                                the decimal point.
     * @param {Integer|String} [right] number of digits to include to the right
     *                                 of the decimal point.
     * @return {String} formatted string.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textSize(16);
     *
     *   // Create a number variable.
     *   let number = 123.45;
     *
     *   // Display the number as a string.
     *   let formatted = nf(number);
     *   text(formatted, 20, 20);
     *
     *   let negative = nf(-number, 4, 2);
     *   text(negative, 20, 40);
     *
     *   // Display the number with four digits
     *   // to the left of the decimal.
     *   let left = nf(number, 4);
     *   text(left, 20, 60);
     *
     *   // Display the number with four digits
     *   // to the left of the decimal and one
     *   // to the right.
     *   let right = nf(number, 4, 1);
     *   text(right, 20, 80);
     *
     *   describe(
     *     'The numbers "123.45", "-0123.45", "0123.45", and "0123.5" written on four separate lines. The text is in black on a gray background.'
     *   );
     * }
     */
    /**
     * @method nf
     * @param {Number[]} nums numbers to format.
     * @param {Integer|String} [left]
     * @param {Integer|String} [right]
     * @return {String[]} formatted strings.
     */
    fn.nf = function(nums, left, right) {
      // p5._validateParameters('nf', arguments);
      if (nums instanceof Array) {
        return nums.map(x => doNf(x, left, right));
      } else {
        const typeOfFirst = Object.prototype.toString.call(nums);
        if (typeOfFirst === '[object Arguments]') {
          if (nums.length === 3) {
            return this.nf(nums[0], nums[1], nums[2]);
          } else if (nums.length === 2) {
            return this.nf(nums[0], nums[1]);
          } else {
            return this.nf(nums[0]);
          }
        } else {
          return doNf(nums, left, right);
        }
      }
    };

    function doNf(num, left, right) {
      let isNegative = num < 0;
      num = Math.abs(num);
      let [leftPart, rightPart] = num.toString().split('.');

      if (typeof right === 'undefined') {
        leftPart = leftPart.padStart(left, '0');
        let result = rightPart ? leftPart + '.' + rightPart : leftPart;
        return isNegative ? '-' + result : result;
      } else {
        let roundedOff = num.toFixed(right);
        [leftPart, rightPart] = roundedOff.toString().split('.');
        leftPart = leftPart.padStart(left, '0');
        let result = typeof rightPart === 'undefined' ? leftPart : leftPart + '.' + rightPart;
        return isNegative ? '-' + result : result;
      }
    }

    /**
     * Converts a `Number` into a `String` with commas to mark units of 1,000.
     *
     * `nfc()` converts numbers such as 12345 into strings formatted with commas
     * to mark the thousands place, as in `'12,345'`.
     *
     * The first parameter, `num`, is the number to convert to a string. For
     * example, calling `nfc(12345)` returns the string `'12,345'`.
     *
     * The second parameter, `right`, is optional. If a number is passed, as in
     * `nfc(12345, 1)`, it sets the minimum number of digits to include to the
     * right of the decimal place. If `right` is smaller than the number of
     * decimal places in `num`, then `num` will be rounded to the given number of
     * decimal places. For example, calling `nfc(12345.67, 1)` returns the string
     * `'12,345.7'`. If `right` is larger than the number of decimal places in
     * `num`, then unused decimal places will be set to 0. For example, calling
     * `nfc(12345.67, 3)` returns the string `'12,345.670'`.
     *
     * @method nfc
     * @param  {Number|String} num number to format.
     * @param  {Integer|String} [right] number of digits to include to the right
     *                                  of the decimal point.
     * @return {String} formatted string.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textSize(16);
     *
     *   // Create a number variable.
     *   let number = 12345;
     *
     *   // Display the number as a string.
     *   let commas = nfc(number);
     *   text(commas, 15, 33);
     *
     *   // Display the number with four digits
     *   // to the left of the decimal.
     *   let decimals = nfc(number, 2);
     *   text(decimals, 15, 67);
     *
     *   describe(
     *     'The numbers "12,345" and "12,345.00" written on separate lines. The text is in black on a gray background.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an array of numbers.
     *   let numbers = [12345, 6789];
     *
     *   // Convert the numbers to formatted strings.
     *   let formatted = nfc(numbers);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(14);
     *
     *   // Iterate over the array.
     *   for (let i = 0; i < formatted.length; i += 1) {
     *
     *     // Calculate the y-coordinate.
     *     let y = (i + 1) * 33;
     *
     *     // Display the original and formatted numbers.
     *     text(`${numbers[i]} : ${formatted[i]}`, 50, y);
     *   }
     *
     *   describe(
     *     'The text "12345 : 12,345" and "6789 : 6,789" written on two separate lines. The text is in black on a gray background.'
     *   );
     * }
     */
    /**
     * @method nfc
     * @param  {Number[]} nums numbers to format.
     * @param  {Integer|String} [right]
     * @return {String[]} formatted strings.
     */
    fn.nfc = function(num, right) {
      // p5._validateParameters('nfc', arguments);
      if (num instanceof Array) {
        return num.map(x => doNfc(x, right));
      } else {
        return doNfc(num, right);
      }
    };
    function doNfc(num, right) {
      num = num.toString();
      const dec = num.indexOf('.');
      let rem = dec !== -1 ? num.substring(dec) : '';
      let n = dec !== -1 ? num.substring(0, dec) : num;
      n = n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
      if (right === 0) {
        rem = '';
      } else if (typeof right !== 'undefined') {
        if (right > rem.length) {
          rem += dec === -1 ? '.' : '';
          const len = right - rem.length + 1;
          for (let i = 0; i < len; i++) {
            rem += '0';
          }
        } else {
          rem = rem.substring(0, right + 1);
        }
      }
      return n + rem;
    }

    /**
     * Converts a `Number` into a `String` with a plus or minus sign.
     *
     * `nfp()` converts numbers such as 123 into strings formatted with a `+` or
     * `-` symbol to mark whether they're positive or negative, as in `'+123'`.
     *
     * The first parameter, `num`, is the number to convert to a string. For
     * example, calling `nfp(123.45)` returns the string `'+123.45'`. If an array
     * of numbers is passed, as in `nfp([123.45, -6.78])`, an array of formatted
     * strings will be returned.
     *
     * The second parameter, `left`, is optional. If a number is passed, as in
     * `nfp(123.45, 4)`, it sets the minimum number of digits to include to the
     * left of the decimal place. If `left` is larger than the number of digits in
     * `num`, then unused digits will be set to 0. For example, calling
     * `nfp(123.45, 4)` returns the string `'+0123.45'`.
     *
     * The third parameter, `right`, is also optional. If a number is passed, as
     * in `nfp(123.45, 4, 1)`, it sets the minimum number of digits to include to
     * the right of the decimal place. If `right` is smaller than the number of
     * decimal places in `num`, then `num` will be rounded to the given number of
     * decimal places.  For example, calling `nfp(123.45, 4, 1)` returns the
     * string `'+0123.5'`. If `right` is larger than the number of decimal places
     * in `num`, then unused decimal places will be set to 0.  For example,
     * calling `nfp(123.45, 4, 3)` returns the string `'+0123.450'`.
     *
     * @method nfp
     * @param {Number} num number to format.
     * @param {Integer} [left] number of digits to include to the left of the
     *                         decimal point.
     * @param {Integer} [right] number of digits to include to the right of the
     *                          decimal point.
     * @return {String} formatted string.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create number variables.
     *   let positive = 123;
     *   let negative = -123;
     *
     *   // Convert the positive number to a formatted string.
     *   let p = nfp(positive);
     *
     *   // Convert the negative number to a formatted string
     *   // with four digits to the left of the decimal
     *   // and two digits to the right of the decimal.
     *   let n = nfp(negative, 4, 2);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(14);
     *
     *   // Display the original and formatted numbers.
     *   text(`${positive} : ${p}`, 50, 33);
     *   text(`${negative} : ${n}`, 50, 67);
     *
     *   describe(
     *     'The text "123 : +123" and "-123 : -123.00" written on separate lines. The text is in black on a gray background.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create number variables.
     *   let numbers = [123, -4.56];
     *
     *   // Convert the numbers to formatted strings
     *   // with four digits to the left of the decimal
     *   // and one digit to the right of the decimal.
     *   let formatted = nfp(numbers, 4, 1);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textSize(14);
     *
     *   // Iterate over the array.
     *   for (let i = 0; i < formatted.length; i += 1) {
     *
     *     // Calculate the y-coordinate.
     *     let y = (i + 1) * 33;
     *
     *     // Display the original and formatted numbers.
     *     text(`${numbers[i]} : ${formatted[i]}`, 50, y);
     *   }
     *
     *   describe(
     *     'The text "123 : +0123.0" and "-4.56 : 00-4.6" written on separate lines. The text is in black on a gray background.'
     *   );
     * }
     */
    /**
     * @method nfp
     * @param {Number[]} nums numbers to format.
     * @param {Integer} [left]
     * @param {Integer} [right]
     * @return {String[]} formatted strings.
     */
    fn.nfp = function(...args) {
      // p5._validateParameters('nfp', args);
      const nfRes = fn.nf.apply(this, args);
      if (nfRes instanceof Array) {
        return nfRes.map(addNfp);
      } else {
        return addNfp(nfRes);
      }
    };

    function addNfp(num) {
      return parseFloat(num) > 0 ? `+${num.toString()}` : num.toString();
    }

    /**
     * Converts a positive `Number` into a `String` with an extra space in front.
     *
     * `nfs()` converts positive numbers such as 123.45 into strings formatted
     * with an extra space in front, as in ' 123.45'. Doing so can be helpful for
     * aligning positive and negative numbers.
     *
     * The first parameter, `num`, is the number to convert to a string. For
     * example, calling `nfs(123.45)` returns the string `' 123.45'`.
     *
     * The second parameter, `left`, is optional. If a number is passed, as in
     * `nfs(123.45, 4)`, it sets the minimum number of digits to include to the
     * left of the decimal place. If `left` is larger than the number of digits in
     * `num`, then unused digits will be set to 0. For example, calling
     * `nfs(123.45, 4)` returns the string `' 0123.45'`.
     *
     * The third parameter, `right`, is also optional. If a number is passed, as
     * in `nfs(123.45, 4, 1)`, it sets the minimum number of digits to include to
     * the right of the decimal place. If `right` is smaller than the number of
     * decimal places in `num`, then `num` will be rounded to the given number of
     * decimal places.  For example, calling `nfs(123.45, 4, 1)` returns the
     * string `' 0123.5'`. If `right` is larger than the number of decimal places
     * in `num`, then unused decimal places will be set to 0.  For example,
     * calling `nfs(123.45, 4, 3)` returns the string `' 0123.450'`.
     *
     * @method nfs
     * @param {Number} num number to format.
     * @param {Integer} [left] number of digits to include to the left of the
     *                         decimal point.
     * @param {Integer} [right] number of digits to include to the right of the
     *                          decimal point.
     * @return {String} formatted string.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create number variables.
     *   let positive = 123;
     *   let negative = -123;
     *
     *   // Convert the positive number to a formatted string.
     *   let formatted = nfs(positive);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(16);
     *
     *   // Display the negative number and the formatted positive number.
     *   text(negative, 50, 33);
     *   text(formatted, 50, 67);
     *
     *   describe(
     *     'The numbers -123 and 123 written on separate lines. The numbers align vertically. The text is in black on a gray background.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a number variable.
     *   let number = 123.45;
     *
     *   // Convert the positive number to a formatted string.
     *   // Use four digits to the left of the decimal and
     *   // one digit to the right.
     *   let formatted = nfs(number, 4, 1);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(16);
     *
     *   // Display a negative version of the number and
     *   // the formatted positive version.
     *   text('-0123.5', 50, 33);
     *   text(formatted, 50, 67);
     *
     *   describe(
     *     'The numbers "-0123.5" and "0123.5" written on separate lines. The numbers align vertically. The text is in black on a gray background.'
     *   );
     * }
     */
    /**
     * @method nfs
     * @param {Array} nums numbers to format.
     * @param {Integer} [left]
     * @param {Integer} [right]
     * @return {String[]} formatted strings.
     */
    fn.nfs = function(...args) {
      // p5._validateParameters('nfs', args);
      const nfRes = fn.nf.apply(this, args);
      if (nfRes instanceof Array) {
        return nfRes.map(addNfs);
      } else {
        return addNfs(nfRes);
      }
    };

    function addNfs(num) {
      return parseFloat(num) >= 0 ? ` ${num.toString()}` : num.toString();
    }

    /**
     * Splits a `String` into pieces and returns an array containing the pieces.
     *
     * `splitTokens()` is an enhanced version of
     * <a href="#/p5/split">split()</a>. It can split a string when any characters
     * from a list are detected.
     *
     * The first parameter, `value`, is the string to split.
     *
     * The second parameter, `delim`, is optional. It sets the character(s) that
     * should be used to split the string. `delim` can be a single string, as in
     * `splitTokens('rock...paper...scissors...shoot', '...')`, or an array of
     * strings, as in
     * `splitTokens('rock;paper,scissors...shoot, [';', ',', '...'])`. By default,
     * if no `delim` characters are specified, then any whitespace character is
     * used to split. Whitespace characters include tab (`\t`), line feed (`\n`),
     * carriage return (`\r`), form feed (`\f`), and space.
     *
     * @method splitTokens
     * @deprecated This will be removed in a future version of p5.js to make way for a new, friendlier version :)
     * @param  {String} value string to split.
     * @param  {String} [delim] character(s) to use for splitting the string.
     * @return {String[]} separated strings.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a string variable.
     *   let string = 'rock paper scissors shoot';
     *
     *   // Split the string at each space.
     *   let words = splitTokens(string);
     *
     *   // Print the array to the console.
     *   print(words);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(12);
     *
     *   // Iterate over the words array.
     *   for (let i = 0; i < words.length; i += 1) {
     *
     *     // Calculate the y-coordinate.
     *     let y = (i + 1) * 20;
     *
     *     // Display the word.
     *     text(words[i], 50, y);
     *   }
     *
     *   describe(
     *     'The words "rock", "paper", "scissors", and "shoot" written on separate lines. The text is black on a gray background.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a string variable.
     *   let string = 'rock...paper...scissors...shoot';
     *
     *   // Split the string at each ...
     *   let words = splitTokens(string, '...');
     *
     *   // Print the array to the console.
     *   print(words);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(12);
     *
     *   // Iterate over the words array.
     *   for (let i = 0; i < words.length; i += 1) {
     *
     *     // Calculate the y-coordinate.
     *     let y = (i + 1) * 20;
     *
     *     // Display the word.
     *     text(words[i], 50, y);
     *   }
     *
     *   describe(
     *     'The words "rock", "paper", "scissors", and "shoot" written on separate lines. The text is black on a gray background.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create a string variable.
     *   let string = 'rock;paper,scissors...shoot';
     *
     *   // Split the string at each semicolon, comma, or ...
     *   let words = splitTokens(string, [';', ',', '...']);
     *
     *   // Print the array to the console.
     *   print(words);
     *
     *   // Style the text.
     *   textAlign(CENTER, CENTER);
     *   textFont('Courier New');
     *   textSize(12);
     *
     *   // Iterate over the words array.
     *   for (let i = 0; i < words.length; i += 1) {
     *
     *     // Calculate the y-coordinate.
     *     let y = (i + 1) * 20;
     *
     *     // Display the word.
     *     text(words[i], 50, y);
     *   }
     *
     *   describe(
     *     'The words "rock", "paper", "scissors", and "shoot" written on separate lines. The text is black on a gray background.'
     *   );
     * }
     */
    fn.splitTokens = function(value, delims) {
      // p5._validateParameters('splitTokens', arguments);
      let d;
      if (typeof delims !== 'undefined') {
        let str = delims;
        const sqc = /\]/g.exec(str);
        let sqo = /\[/g.exec(str);
        if (sqo && sqc) {
          str = str.slice(0, sqc.index) + str.slice(sqc.index + 1);
          sqo = /\[/g.exec(str);
          str = str.slice(0, sqo.index) + str.slice(sqo.index + 1);
          d = new RegExp(`[\\[${str}\\]]`, 'g');
        } else if (sqc) {
          str = str.slice(0, sqc.index) + str.slice(sqc.index + 1);
          d = new RegExp(`[${str}\\]]`, 'g');
        } else if (sqo) {
          str = str.slice(0, sqo.index) + str.slice(sqo.index + 1);
          d = new RegExp(`[${str}\\[]`, 'g');
        } else {
          d = new RegExp(`[${str}]`, 'g');
        }
      } else {
        d = /\s/g;
      }
      return value.split(d).filter(n => n);
    };

    /**
     * Shuffles the elements of an array.
     *
     * The first parameter, `array`, is the array to be shuffled. For example,
     * calling `shuffle(myArray)` will shuffle the elements of `myArray`. By
     * default, the original array won’t be modified. Instead, a copy will be
     * created, shuffled, and returned.
     *
     * The second parameter, `modify`, is optional. If `true` is passed, as in
     * `shuffle(myArray, true)`, then the array will be shuffled in place without
     * making a copy.
     *
     * @method shuffle
     * @param  {Array} array array to shuffle.
     * @param  {Boolean} [modify] if `true`, shuffle the original array in place. Defaults to `false`.
     * @return {Array} shuffled array.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an array of colors.
     *   let colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'];
     *
     *   // Create a shuffled copy of the array.
     *   let shuffledColors = shuffle(colors);
     *
     *   // Draw  a row of circles using the original array.
     *   for (let i = 0; i < colors.length; i += 1) {
     *     // Calculate the x-coordinate.
     *     let x = (i + 1) * 12.5;
     *
     *     // Style the circle.
     *     let c = colors[i];
     *     fill(c);
     *
     *     // Draw the circle.
     *     circle(x, 33, 10);
     *   }
     *
     *   // Draw  a row of circles using the original array.
     *   for (let i = 0; i < shuffledColors.length; i += 1) {
     *     // Calculate the x-coordinate.
     *     let x = (i + 1) * 12.5;
     *
     *     // Style the circle.
     *     let c = shuffledColors[i];
     *     fill(c);
     *
     *     // Draw the circle.
     *     circle(x, 67, 10);
     *   }
     *
     *   describe(
     *     'Two rows of circles on a gray background. The top row follows the color sequence ROYGBIV. The bottom row has all the same colors but they are shuffled.'
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Create an array of colors.
     *   let colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'];
     *
     *   // Shuffle the array.
     *   shuffle(colors, true);
     *
     *   // Draw  a row of circles using the original array.
     *   for (let i = 0; i < colors.length; i += 1) {
     *     // Calculate the x-coordinate.
     *     let x = (i + 1) * 12.5;
     *
     *     // Style the circle.
     *     let c = colors[i];
     *     fill(c);
     *
     *     // Draw the circle.
     *     circle(x, 50, 10);
     *   }
     *
     *   describe(
     *     'A row of colorful circles on a gray background. Their sequence changes each time the sketch runs.'
     *   );
     * }
     */
    fn.shuffle = function (arr, modify) {
      const isView = ArrayBuffer && ArrayBuffer.isView && ArrayBuffer.isView(arr);
      arr = modify || isView ? arr : arr.slice();

      let rnd,
        tmp,
        idx = arr.length;
      while (idx > 1) {
        rnd = (this.random(0, 1) * idx) | 0;

        tmp = arr[--idx];
        arr[idx] = arr[rnd];
        arr[rnd] = tmp;
      }

      return arr;
    };
  }

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

  /**
   * @module IO
   * @submodule Time & Date
   * @for p5
   * @requires core
   */

  function timeDate(p5, fn){
    /**
     * Returns the current day as a number from 1–31.
     *
     * @method day
     * @return {Integer} current day between 1 and 31.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the current day.
     *   let d = day();
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textSize(12);
     *   textFont('Courier New');
     *
     *   // Display the day.
     *   text(`Current day: ${d}`, 20, 50, 60);
     *
     *   describe(`The text 'Current day: ${d}' written in black on a gray background.`);
     * }
     */
    fn.day = function() {
      return new Date().getDate();
    };

    /**
     * Returns the current hour as a number from 0–23.
     *
     * @method hour
     * @return {Integer} current hour between 0 and 23.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the current hour.
     *   let h = hour();
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textSize(12);
     *   textFont('Courier New');
     *
     *   // Display the hour.
     *   text(`Current hour: ${h}`, 20, 50, 60);
     *
     *   describe(`The text 'Current hour: ${h}' written in black on a gray background.`);
     * }
     */
    fn.hour = function() {
      return new Date().getHours();
    };

    /**
     * Returns the current minute as a number from 0–59.
     *
     * @method minute
     * @return {Integer} current minute between 0 and 59.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the current minute.
     *   let m = minute();
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textSize(12);
     *   textFont('Courier New');
     *
     *   // Display the minute.
     *   text(`Current minute: ${m}`, 10, 50, 80);
     *
     *   describe(`The text 'Current minute: ${m}' written in black on a gray background.`);
     * }
     */
    fn.minute = function() {
      return new Date().getMinutes();
    };

    /**
     * Returns the number of milliseconds since a sketch started running.
     *
     * `millis()` keeps track of how long a sketch has been running in
     * milliseconds (thousandths of a second). This information is often
     * helpful for timing events and animations.
     *
     * If a sketch has a
     * <a href="#/p5/setup">setup()</a> function, then `millis()` begins tracking
     * time before the code in <a href="#/p5/setup">setup()</a> runs.  If a
     * sketch includes asynchronous loading using `async`/`await`, then
     * `millis()` begins tracking time as soon as the asynchronous code
     * starts running.
     * @method millis
     * @return {Number} number of milliseconds since starting the sketch.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the number of milliseconds the sketch has run.
     *   let ms = millis();
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textSize(10);
     *   textFont('Courier New');
     *
     *   // Display how long it took setup() to be called.
     *   text(`Startup time: ${round(ms, 2)} ms`, 5, 50, 90);
     *
     *   describe(
     *     `The text 'Startup time: ${round(ms, 2)} ms' written in black on a gray background.`
     *   );
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('The text "Running time: S sec" written in black on a gray background. The number S increases as the sketch runs.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Get the number of seconds the sketch has run.
     *   let s = millis() / 1000;
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textSize(10);
     *   textFont('Courier New');
     *
     *   // Display how long the sketch has run.
     *   text(`Running time: ${nf(s, 1, 1)} sec`, 5, 50, 90);
     * }
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   describe('A white circle oscillates left and right on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Get the number of seconds the sketch has run.
     *   let s = millis() / 1000;
     *
     *   // Calculate an x-coordinate.
     *   let x = 30 * sin(s) + 50;
     *
     *   // Draw the circle.
     *   circle(x, 50, 30);
     * }
     *
     * @example
     * async function setup() {
     *   // Load the GeoJSON.
     *   await loadJSON('https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson');
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the number of milliseconds the sketch has run.
     *   let ms = millis();
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textFont('Courier New');
     *   textSize(11);
     *
     *   // Display how long it took to load the data.
     *   text(`It took ${round(ms, 2)} ms to load the data`, 5, 50, 100);
     *
     *   describe(
     *     `The text "It took ${round(ms, 2)} ms to load the data" written in black on a gray background.`
     *   );
     * }
     */
    fn.millis = function() {
      if (this._millisStart === -1) {
        // Sketch has not started
        return 0;
      } else {
        return window.performance.now() - this._millisStart;
      }
    };

    /**
     * Returns the current month as a number from 1–12.
     *
     * @method month
     * @return {Integer} current month between 1 and 12.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the current month.
     *   let m = month();
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textSize(12);
     *   textFont('Courier New');
     *
     *   // Display the month.
     *   text(`Current month: ${m}`, 10, 50, 80);
     *
     *   describe(`The text 'Current month: ${m}' written in black on a gray background.`);
     * }
     */
    fn.month = function() {
      //January is 0!
      return new Date().getMonth() + 1;
    };

    /**
     * Returns the current second as a number from 0–59.
     *
     * @method second
     * @return {Integer} current second between 0 and 59.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the current second.
     *   let s = second();
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textSize(12);
     *   textFont('Courier New');
     *
     *   // Display the second.
     *   text(`Current second: ${s}`, 10, 50, 80);
     *
     *   describe(`The text 'Current second: ${s}' written in black on a gray background.`);
     * }
     */
    fn.second = function() {
      return new Date().getSeconds();
    };

    /**
     * Returns the current year as a number such as 1999.
     *
     * @method year
     * @return {Integer} current year.
     *
     * @example
     * function setup() {
     *   createCanvas(100, 100);
     *
     *   background(200);
     *
     *   // Get the current year.
     *   let y = year();
     *
     *   // Style the text.
     *   textAlign(LEFT, CENTER);
     *   textSize(12);
     *   textFont('Courier New');
     *
     *   // Display the year.
     *   text(`Current year: ${y}`, 10, 50, 80);
     *
     *   describe(`The text 'Current year: ${y}' written in black on a gray background.`);
     * }
     */
    fn.year = function() {
      return new Date().getFullYear();
    };
  }

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

  function utilities(p5){
    p5.registerAddon(conversion);
    p5.registerAddon(utilityFunctions);
    p5.registerAddon(timeDate);
  }

  /**
   * @module 3D
   * @submodule Interaction
   * @for p5
   * @requires core
   */


  function interaction(p5, fn){
    /**
     * Allows the user to orbit around a 3D sketch using a mouse, trackpad, or
     * touchscreen.
     *
     * 3D sketches are viewed through an imaginary camera. Calling
     * `orbitControl()` within the <a href="#/p5/draw">draw()</a> function allows
     * the user to change the camera’s position:
     *
     * ```js
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Rest of sketch.
     * }
     * ```
     *
     * Left-clicking and dragging or swipe motion will rotate the camera position
     * about the center of the sketch. Right-clicking and dragging or multi-swipe
     * will pan the camera position without rotation. Using the mouse wheel
     * (scrolling) or pinch in/out will move the camera further or closer from the
     * center of the sketch.
     *
     * The first three parameters, `sensitivityX`, `sensitivityY`, and
     * `sensitivityZ`, are optional. They’re numbers that set the sketch’s
     * sensitivity to movement along each axis. For example, calling
     * `orbitControl(1, 2, -1)` keeps movement along the x-axis at its default
     * value, makes the sketch twice as sensitive to movement along the y-axis,
     * and reverses motion along the z-axis. By default, all sensitivity values
     * are 1.
     *
     * The fourth parameter, `options`, is also optional. It’s an object that
     * changes the behavior of orbiting. For example, calling
     * `orbitControl(1, 1, 1, options)` keeps the default sensitivity values while
     * changing the behaviors set with `options`. The object can have the
     * following properties:
     *
     * ```js
     * let options = {
     *   // Setting this to false makes mobile interactions smoother by
     *   // preventing accidental interactions with the page while orbiting.
     *   // By default, it's true.
     *   disableTouchActions: true,
     *
     *   // Setting this to true makes the camera always rotate in the
     *   // direction the mouse/touch is moving.
     *   // By default, it's false.
     *   freeRotation: false
     * };
     *
     * orbitControl(1, 1, 1, options);
     * ```
     *
     * @method orbitControl
     * @for p5
     * @param  {Number} [sensitivityX] sensitivity to movement along the x-axis. Defaults to 1.
     * @param  {Number} [sensitivityY] sensitivity to movement along the y-axis. Defaults to 1.
     * @param  {Number} [sensitivityZ] sensitivity to movement along the z-axis. Defaults to 1.
     * @param  {Object} [options] Settings for orbitControl:
     * @param {Boolean} [options.disableTouchActions=true] Prevent accidental interactions with the page while orbiting.
     * @param {Boolean} [options.freeRotation=false] Rotate in the drag direction instead of on principal axes.
     * @chainable
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A multicolor box on a gray background. The camera angle changes when the user interacts using a mouse, trackpad, or touchscreen.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Style the box.
     *   normalMaterial();
     *
     *   // Draw the box.
     *   box(30, 50);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A multicolor box on a gray background. The camera angle changes when the user interacts using a mouse, trackpad, or touchscreen.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   // Make the interactions 3X sensitive.
     *   orbitControl(3, 3, 3);
     *
     *   // Style the box.
     *   normalMaterial();
     *
     *   // Draw the box.
     *   box(30, 50);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A multicolor box on a gray background. The camera angle changes when the user interacts using a mouse, trackpad, or touchscreen.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Create an options object.
     *   let options = {
     *     disableTouchActions: false,
     *     freeRotation: true
     *   };
     *
     *   // Enable orbiting with the mouse.
     *   // Prevent accidental touch actions on touchscreen devices
     *   // and enable free rotation.
     *   orbitControl(1, 1, 1, options);
     *
     *   // Style the box.
     *   normalMaterial();
     *
     *   // Draw the box.
     *   box(30, 50);
     * }
     */

    // implementation based on three.js 'orbitControls':
    // https://github.com/mrdoob/three.js/blob/6afb8595c0bf8b2e72818e42b64e6fe22707d896/examples/jsm/controls/OrbitControls.js#L22
    fn.orbitControl = function(
      sensitivityX,
      sensitivityY,
      sensitivityZ,
      options
    ) {
      this._assert3d('orbitControl');
      // p5._validateParameters('orbitControl', arguments);

      const cam = this._renderer.states.curCamera;

      if (typeof sensitivityX === 'undefined') {
        sensitivityX = 1;
      }
      if (typeof sensitivityY === 'undefined') {
        sensitivityY = sensitivityX;
      }
      if (typeof sensitivityZ === 'undefined') {
        sensitivityZ = 1;
      }
      if (typeof options !== 'object') {
        options = {};
      }

      // default right-mouse and mouse-wheel behaviors (context menu and scrolling,
      // respectively) are disabled here to allow use of those events for panning and
      // zooming. However, whether or not to disable touch actions is an option.

      // disable context menu for canvas element and add 'contextMenuDisabled'
      // flag to p5 instance
      if (this.contextMenuDisabled !== true) {
        this.canvas.oncontextmenu = () => false;
        this.contextMenuDisabled = true;
      }

      // disable default scrolling behavior on the canvas element and add
      // 'wheelDefaultDisabled' flag to p5 instance
      if (this.wheelDefaultDisabled !== true) {
        this.canvas.onwheel = () => false;
        this.wheelDefaultDisabled = true;
      }

      // disable default touch behavior on the canvas element and add
      // 'touchActionsDisabled' flag to p5 instance
      const { disableTouchActions = true } = options;
      if (this.touchActionsDisabled !== true && disableTouchActions) {
        this.canvas.style['touch-action'] = 'none';
        this.touchActionsDisabled = true;
      }

      // If option.freeRotation is true, the camera always rotates freely in the direction
      // the pointer moves. default value is false (normal behavior)
      const { freeRotation = false } = options;

      // get moved touches.
      const movedTouches = [];

      this.touches.forEach(curTouch => {
        this._renderer.prevTouches.forEach(prevTouch => {
          if (curTouch.id === prevTouch.id) {
            const movedTouch = {
              x: curTouch.x,
              y: curTouch.y,
              px: prevTouch.x,
              py: prevTouch.y
            };
            movedTouches.push(movedTouch);
          }
        });
      });

      this._renderer.prevTouches = this.touches;

      // The idea of using damping is based on the following website. thank you.
      // https://github.com/freshfork/p5.EasyCam/blob/9782964680f6a5c4c9bee825c475d9f2021d5134/p5.easycam.js#L1124

      // variables for interaction
      let deltaRadius = 0;
      let deltaTheta = 0;
      let deltaPhi = 0;
      let moveDeltaX = 0;
      let moveDeltaY = 0;
      // constants for dampingProcess
      const damping = 0.85;
      const rotateAccelerationFactor = 0.6;
      const moveAccelerationFactor = 0.15;
      // For touches, the appropriate scale is different
      // because the distance difference is multiplied.
      const mouseZoomScaleFactor = 0.01;
      const touchZoomScaleFactor = 0.0004;
      const scaleFactor = this.height < this.width ? this.height : this.width;
      // Flag whether the mouse or touch pointer is inside the canvas
      let pointersInCanvas = false;

      // calculate and determine flags and variables.
      if (movedTouches.length > 0) {
        /* for touch */
        // if length === 1, rotate
        // if length > 1, zoom and move

        // for touch, it is calculated based on one moved touch pointer position.
        pointersInCanvas =
          movedTouches[0].x > 0 && movedTouches[0].x < this.width &&
          movedTouches[0].y > 0 && movedTouches[0].y < this.height;

        if (movedTouches.length === 1) {
          const t = movedTouches[0];
          deltaTheta = -sensitivityX * (t.x - t.px) / scaleFactor;
          deltaPhi = sensitivityY * (t.y - t.py) / scaleFactor;
        } else {
          const t0 = movedTouches[0];
          const t1 = movedTouches[1];
          const distWithTouches = Math.hypot(t0.x - t1.x, t0.y - t1.y);
          const prevDistWithTouches = Math.hypot(t0.px - t1.px, t0.py - t1.py);
          const changeDist = distWithTouches - prevDistWithTouches;
          // move the camera farther when the distance between the two touch points
          // decreases, move the camera closer when it increases.
          deltaRadius = -changeDist * sensitivityZ * touchZoomScaleFactor;
          // Move the center of the camera along with the movement of
          // the center of gravity of the two touch points.
          moveDeltaX = 0.5 * (t0.x + t1.x) - 0.5 * (t0.px + t1.px);
          moveDeltaY = 0.5 * (t0.y + t1.y) - 0.5 * (t0.py + t1.py);
        }
        if (this.touches.length > 0) {
          if (pointersInCanvas) {
            // Initiate an interaction if touched in the canvas
            this._renderer.executeRotateAndMove = true;
            this._renderer.executeZoom = true;
          }
        } else {
          // End an interaction when the touch is released
          this._renderer.executeRotateAndMove = false;
          this._renderer.executeZoom = false;
        }
      } else {
        /* for mouse */
        // if wheelDeltaY !== 0, zoom
        // if mouseLeftButton is down, rotate
        // if mouseRightButton is down, move

        // For mouse, it is calculated based on the mouse position.
        pointersInCanvas =
          (this.mouseX > 0 && this.mouseX < this.width) &&
          (this.mouseY > 0 && this.mouseY < this.height);

        if (this._mouseWheelDeltaY !== 0) {
          // zoom the camera depending on the value of _mouseWheelDeltaY.
          // move away if positive, move closer if negative
          deltaRadius = Math.sign(this._mouseWheelDeltaY) * sensitivityZ;
          deltaRadius *= mouseZoomScaleFactor;
          this._mouseWheelDeltaY = 0;
          // start zoom when the mouse is wheeled within the canvas.
          if (pointersInCanvas) this._renderer.executeZoom = true;
        } else {
          // quit zoom when you stop wheeling.
          this._renderer.executeZoom = false;
        }
        if (this.mouseIsPressed) {
          if (this.mouseButton.left) {
            deltaTheta = -sensitivityX * this.movedX / scaleFactor;
            deltaPhi = sensitivityY * this.movedY / scaleFactor;
          } else if (this.mouseButton.right) {
            moveDeltaX = this.movedX;
            moveDeltaY =  this.movedY * cam.yScale;
          }
          // start rotate and move when mouse is pressed within the canvas.
          if (pointersInCanvas) this._renderer.executeRotateAndMove = true;
        } else {
          // quit rotate and move if mouse is released.
          this._renderer.executeRotateAndMove = false;
        }
      }

      // interactions

      // zoom process
      if (deltaRadius !== 0 && this._renderer.executeZoom) {
        // accelerate zoom velocity
        this._renderer.zoomVelocity += deltaRadius;
      }
      if (Math.abs(this._renderer.zoomVelocity) > 0.001) {
        // if freeRotation is true, we use _orbitFree() instead of _orbit()
        if (freeRotation) {
          cam._orbitFree(
            0, 0, this._renderer.zoomVelocity
          );
        } else {
          cam._orbit(
            0, 0, this._renderer.zoomVelocity
          );
        }
        // In orthogonal projection, the scale does not change even if
        // the distance to the gaze point is changed, so the projection matrix
        // needs to be modified.
        if (cam.projMatrix.mat4[15] !== 0) {
          cam.projMatrix.mat4[0] *= Math.pow(
            10, -this._renderer.zoomVelocity
          );
          cam.projMatrix.mat4[5] *= Math.pow(
            10, -this._renderer.zoomVelocity
          );
          // modify uPMatrix
          this._renderer.states.setValue('uPMatrix', this._renderer.states.uPMatrix.clone());
          this._renderer.states.uPMatrix.mat4[0] = cam.projMatrix.mat4[0];
          this._renderer.states.uPMatrix.mat4[5] = cam.projMatrix.mat4[5];
        }
        // damping
        this._renderer.zoomVelocity *= damping;
      } else {
        this._renderer.zoomVelocity = 0;
      }

      // rotate process
      if ((deltaTheta !== 0 || deltaPhi !== 0) &&
      this._renderer.executeRotateAndMove) {
        // accelerate rotate velocity
        this._renderer.rotateVelocity.add(
          deltaTheta * rotateAccelerationFactor,
          deltaPhi * rotateAccelerationFactor
        );
      }
      if (this._renderer.rotateVelocity.magSq() > 0.000001) {
        // if freeRotation is true, the camera always rotates freely in the direction the pointer moves
        if (freeRotation) {
          cam._orbitFree(
            -this._renderer.rotateVelocity.x,
            this._renderer.rotateVelocity.y,
            0
          );
        } else {
          cam._orbit(
            this._renderer.rotateVelocity.x,
            this._renderer.rotateVelocity.y,
            0
          );
        }
        // damping
        this._renderer.rotateVelocity.mult(damping);
      } else {
        this._renderer.rotateVelocity.set(0, 0);
      }

      // move process
      if ((moveDeltaX !== 0 || moveDeltaY !== 0) &&
      this._renderer.executeRotateAndMove) {
        // Normalize movement distance
        const ndcX = moveDeltaX * 2/this.width;
        const ndcY = -moveDeltaY * 2/this.height;
        // accelerate move velocity
        this._renderer.moveVelocity.add(
          ndcX * moveAccelerationFactor,
          ndcY * moveAccelerationFactor
        );
      }
      if (this._renderer.moveVelocity.magSq() > 0.000001) {
        // Translate the camera so that the entire object moves
        // perpendicular to the line of sight when the mouse is moved
        // or when the centers of gravity of the two touch pointers move.
        const local = cam._getLocalAxes();

        // Calculate the z coordinate in the view coordinates of
        // the center, that is, the distance to the view point
        const diffX = cam.eyeX - cam.centerX;
        const diffY = cam.eyeY - cam.centerY;
        const diffZ = cam.eyeZ - cam.centerZ;
        const viewZ = Math.sqrt(diffX * diffX + diffY * diffY + diffZ * diffZ);

        // position vector of the center.
        let cv = new Vector(cam.centerX, cam.centerY, cam.centerZ);

        // Calculate the normalized device coordinates of the center.
        cv = cam.cameraMatrix.multiplyPoint(cv);
        cv = this._renderer.states.uPMatrix.multiplyAndNormalizePoint(cv);

        // Move the center by this distance
        // in the normalized device coordinate system.
        cv.x -= this._renderer.moveVelocity.x;
        cv.y -= this._renderer.moveVelocity.y;

        // Calculate the translation vector
        // in the direction perpendicular to the line of sight of center.
        let dx, dy;
        const uP = this._renderer.states.uPMatrix.mat4;

        if (uP[15] === 0) {
          dx = ((uP[8] + cv.x)/uP[0]) * viewZ;
          dy = ((uP[9] + cv.y)/uP[5]) * viewZ;
        } else {
          dx = (cv.x - uP[12])/uP[0];
          dy = (cv.y - uP[13])/uP[5];
        }

        // translate the camera.
        cam.setPosition(
          cam.eyeX + dx * local.x[0] + dy * local.y[0],
          cam.eyeY + dx * local.x[1] + dy * local.y[1],
          cam.eyeZ + dx * local.x[2] + dy * local.y[2]
        );
        // damping
        this._renderer.moveVelocity.mult(damping);
      } else {
        this._renderer.moveVelocity.set(0, 0);
      }

      return this;
    };


    /**
     * Adds a grid and an axes icon to clarify orientation in 3D sketches.
     *
     * `debugMode()` adds a grid that shows where the “ground” is in a sketch. By
     * default, the grid will run through the origin `(0, 0, 0)` of the sketch
     * along the XZ plane. `debugMode()` also adds an axes icon that points along
     * the positive x-, y-, and z-axes. Calling `debugMode()` displays the grid
     * and axes icon with their default size and position.
     *
     * There are four ways to call `debugMode()` with optional parameters to
     * customize the debugging environment.
     *
     * The first way to call `debugMode()` has one parameter, `mode`. If the
     * system constant `GRID` is passed, as in `debugMode(GRID)`, then the grid
     * will be displayed and the axes icon will be hidden. If the constant `AXES`
     * is passed, as in `debugMode(AXES)`, then the axes icon will be displayed
     * and the grid will be hidden.
     *
     * The second way to call `debugMode()` has six parameters. The first
     * parameter, `mode`, selects either `GRID` or `AXES` to be displayed. The
     * next five parameters, `gridSize`, `gridDivisions`, `xOff`, `yOff`, and
     * `zOff` are optional. They’re numbers that set the appearance of the grid
     * (`gridSize` and `gridDivisions`) and the placement of the axes icon
     * (`xOff`, `yOff`, and `zOff`). For example, calling
     * `debugMode(20, 5, 10, 10, 10)` sets the `gridSize` to 20 pixels, the number
     * of `gridDivisions` to 5, and offsets the axes icon by 10 pixels along the
     * x-, y-, and z-axes.
     *
     * The third way to call `debugMode()` has five parameters. The first
     * parameter, `mode`, selects either `GRID` or `AXES` to be displayed. The
     * next four parameters, `axesSize`, `xOff`, `yOff`, and `zOff` are optional.
     * They’re numbers that set the appearance of the size of the axes icon
     * (`axesSize`) and its placement (`xOff`, `yOff`, and `zOff`).
     *
     * The fourth way to call `debugMode()` has nine optional parameters. The
     * first five parameters, `gridSize`, `gridDivisions`, `gridXOff`, `gridYOff`,
     * and `gridZOff` are numbers that set the appearance of the grid. For
     * example, calling `debugMode(100, 5, 0, 0, 0)` sets the `gridSize` to 100,
     * the number of `gridDivisions` to 5, and sets all the offsets to 0 so that
     * the grid is centered at the origin. The next four parameters, `axesSize`,
     * `xOff`, `yOff`, and `zOff` are numbers that set the appearance of the size
     * of the axes icon (`axesSize`) and its placement (`axesXOff`, `axesYOff`,
     * and `axesZOff`). For example, calling
     * `debugMode(100, 5, 0, 0, 0, 50, 10, 10, 10)` sets the `gridSize` to 100,
     * the number of `gridDivisions` to 5, and sets all the offsets to 0 so that
     * the grid is centered at the origin. It then sets the `axesSize` to 50 and
     * offsets the icon 10 pixels along each axis.
     *
     * @method debugMode
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Enable debug mode.
     *   debugMode();
     *
     *   describe('A multicolor box on a gray background. A grid and axes icon are displayed near the box.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Style the box.
     *   normalMaterial();
     *
     *   // Draw the box.
     *   box(20, 40);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Enable debug mode.
     *   // Only display the axes icon.
     *   debugMode(AXES);
     *
     *   describe('A multicolor box on a gray background. A grid and axes icon are displayed near the box.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Style the box.
     *   normalMaterial();
     *
     *   // Draw the box.
     *   box(20, 40);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Enable debug mode.
     *   // Only display the grid and customize it:
     *   // - size: 50
     *   // - divisions: 10
     *   // - offsets: 0, 20, 0
     *   debugMode(GRID, 50, 10, 0, 20, 0);
     *
     *   describe('A multicolor box on a gray background. A grid is displayed below the box.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Style the box.
     *   normalMaterial();
     *
     *   // Draw the box.
     *   box(20, 40);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Enable debug mode.
     *   // Display the grid and axes icon and customize them:
     *   // Grid
     *   // ----
     *   // - size: 50
     *   // - divisions: 10
     *   // - offsets: 0, 20, 0
     *   // Axes
     *   // ----
     *   // - size: 50
     *   // - offsets: 0, 0, 0
     *   debugMode(50, 10, 0, 20, 0, 50, 0, 0, 0);
     *
     *   describe('A multicolor box on a gray background. A grid is displayed below the box. An axes icon is displayed at the center of the box.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Style the box.
     *   normalMaterial();
     *
     *   // Draw the box.
     *   box(20, 40);
     * }
     */

    /**
     * @method debugMode
     * @param {(GRID|AXES)} mode either GRID or AXES
     */

    /**
     * @method debugMode
     * @param {(GRID|AXES)} mode
     * @param {Number} [gridSize] side length of the grid.
     * @param {Number} [gridDivisions] number of divisions in the grid.
     * @param {Number} [xOff] offset from origin along the x-axis.
     * @param {Number} [yOff] offset from origin along the y-axis.
     * @param {Number} [zOff] offset from origin along the z-axis.
     */

    /**
     * @method debugMode
     * @param {(GRID|AXES)} mode
     * @param {Number} [axesSize] length of axes icon markers.
     * @param {Number} [xOff]
     * @param {Number} [yOff]
     * @param {Number} [zOff]
     */

    /**
     * @method debugMode
     * @param {Number} [gridSize]
     * @param {Number} [gridDivisions]
     * @param {Number} [gridXOff] grid offset from the origin along the x-axis.
     * @param {Number} [gridYOff] grid offset from the origin along the y-axis.
     * @param {Number} [gridZOff] grid offset from the origin along the z-axis.
     * @param {Number} [axesSize]
     * @param {Number} [axesXOff] axes icon offset from the origin along the x-axis.
     * @param {Number} [axesYOff] axes icon offset from the origin along the y-axis.
     * @param {Number} [axesZOff] axes icon offset from the origin along the z-axis.
     */

    fn.debugMode = function(...args) {
      this._assert3d('debugMode');
      // p5._validateParameters('debugMode', args);

      // start by removing existing 'post' registered debug methods
      for (let i = p5.lifecycleHooks.postdraw.length - 1; i >= 0; i--) {
        // test for equality...
        if (
          p5.lifecycleHooks.postdraw[i].toString() === this._grid().toString() ||
          p5.lifecycleHooks.postdraw[i].toString() === this._axesIcon().toString()
        ) {
          p5.lifecycleHooks.postdraw.splice(i, 1);
        }
      }

      // then add new debugMode functions according to the argument list
      if (args[0] === GRID) {
        p5.lifecycleHooks.postdraw.push(
          this._grid(args[1], args[2], args[3], args[4], args[5])
        );
      } else if (args[0] === AXES) {
        p5.lifecycleHooks.postdraw.push(
          this._axesIcon(args[1], args[2], args[3], args[4])
        );
      } else {
        p5.lifecycleHooks.postdraw.push(
          this._grid(args[0], args[1], args[2], args[3], args[4])
        );
        p5.lifecycleHooks.postdraw.push(
          this._axesIcon(args[5], args[6], args[7], args[8])
        );
      }
    };

    /**
     * Turns off <a href="#/p5/debugMode">debugMode()</a> in a 3D sketch.
     *
     * @method noDebugMode
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Enable debug mode.
     *   debugMode();
     *
     *   describe('A multicolor box on a gray background. A grid and axes icon are displayed near the box. They disappear when the user double-clicks.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Style the box.
     *   normalMaterial();
     *
     *   // Draw the box.  box(20, 40);
     * }
     *
     * // Disable debug mode when the user double-clicks.
     * function doubleClicked() {
     *   noDebugMode();
     * }
     */
    fn.noDebugMode = function() {
      this._assert3d('noDebugMode');

      // start by removing existing 'post' registered debug methods
      for (let i = p5.lifecycleHooks.postdraw.length - 1; i >= 0; i--) {
        // test for equality...
        if (
          p5.lifecycleHooks.postdraw[i].toString() === this._grid().toString() ||
          p5.lifecycleHooks.postdraw[i].toString() === this._axesIcon().toString()
        ) {
          p5.lifecycleHooks.postdraw.splice(i, 1);
        }
      }
    };

    /**
     * For use with debugMode
     * @private
     * @method _grid
     * @param {Number} [size] size of grid sides
     * @param {Number} [div] number of grid divisions
     * @param {Number} [xOff] offset of grid center from origin in X axis
     * @param {Number} [yOff] offset of grid center from origin in Y axis
     * @param {Number} [zOff] offset of grid center from origin in Z axis
     */
    fn._grid = function(size, numDivs, xOff, yOff, zOff) {
      if (typeof size === 'undefined') {
        size = this.width / 2;
      }
      if (typeof numDivs === 'undefined') {
        // ensure at least 2 divisions
        numDivs = Math.round(size / 30) < 4 ? 4 : Math.round(size / 30);
      }
      if (typeof xOff === 'undefined') {
        xOff = 0;
      }
      if (typeof yOff === 'undefined') {
        yOff = 0;
      }
      if (typeof zOff === 'undefined') {
        zOff = 0;
      }

      const spacing = size / numDivs;
      const halfSize = size / 2;

      return function() {
        this.push();
        this.stroke(
          this._renderer.states.curStrokeColor[0] * 255,
          this._renderer.states.curStrokeColor[1] * 255,
          this._renderer.states.curStrokeColor[2] * 255
        );
        this._renderer.states.setValue('uModelMatrix', this._renderer.states.uModelMatrix.clone());
        this._renderer.states.uModelMatrix.reset();

        // Lines along X axis
        for (let q = 0; q <= numDivs; q++) {
          this.beginShape(this.LINES);
          this.vertex(-halfSize + xOff, yOff, q * spacing - halfSize + zOff);
          this.vertex(+halfSize + xOff, yOff, q * spacing - halfSize + zOff);
          this.endShape();
        }

        // Lines along Z axis
        for (let i = 0; i <= numDivs; i++) {
          this.beginShape(this.LINES);
          this.vertex(i * spacing - halfSize + xOff, yOff, -halfSize + zOff);
          this.vertex(i * spacing - halfSize + xOff, yOff, +halfSize + zOff);
          this.endShape();
        }

        this.pop();
      };
    };

    /**
     * For use with debugMode
     * @private
     * @method _axesIcon
     * @param {Number} [size] size of axes icon lines
     * @param {Number} [xOff] offset of icon from origin in X axis
     * @param {Number} [yOff] offset of icon from origin in Y axis
     * @param {Number} [zOff] offset of icon from origin in Z axis
     */
    fn._axesIcon = function(size, xOff, yOff, zOff) {
      if (typeof size === 'undefined') {
        size = this.width / 20 > 40 ? this.width / 20 : 40;
      }
      if (typeof xOff === 'undefined') {
        xOff = -this.width / 4;
      }
      if (typeof yOff === 'undefined') {
        yOff = xOff;
      }
      if (typeof zOff === 'undefined') {
        zOff = xOff;
      }

      return () => {
        this.push();
        this._renderer.states.setValue('uModelMatrix', this._renderer.states.uModelMatrix.clone());
        this._renderer.states.uModelMatrix.reset();

        // X axis
        this.strokeWeight(2);
        this.stroke(255, 0, 0);
        this.beginShape(this.LINES);
        this.vertex(xOff, yOff, zOff);
        this.vertex(xOff + size, yOff, zOff);
        this.endShape();
        // Y axis
        this.stroke(0, 255, 0);
        this.beginShape(this.LINES);
        this.vertex(xOff, yOff, zOff);
        this.vertex(xOff, yOff + size, zOff);
        this.endShape();
        // Z axis
        this.stroke(0, 0, 255);
        this.beginShape(this.LINES);
        this.vertex(xOff, yOff, zOff);
        this.vertex(xOff, yOff, zOff + size);
        this.endShape();
        this.pop();
      };
    };
  }

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

  /**
   * @module Shape
   * @submodule 3D Models
   * @for p5
   * @requires core
   * @requires p5.Geometry
   */


  async function fileExists(url) {
    try {
      const response = await fetch(url, { method: 'HEAD' });
      return response.ok;
    } catch (error) {
      return false;
    }
  }

  function loading$1(p5, fn){
    /**
     * Loads a 3D model to create a
     * <a href="#/p5.Geometry">p5.Geometry</a> object.
     *
     * `loadModel()` can load 3D models from OBJ and STL files. Once the model is
     * loaded, it can be displayed with the
     * <a href="#/p5/model">model()</a> function, as in `model(shape)`.
     *
     * There are three ways to call `loadModel()` with optional parameters to help
     * process the model.
     *
     * The first parameter, `path`, is a `String` with the path to the file. Paths
     * to local files should be relative, as in `loadModel('assets/model.obj')`.
     * URLs such as `'https://example.com/model.obj'` may be blocked due to browser
     * security. The `path` parameter can also be defined as a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
     * object for more advanced usage.
     * Note: When loading a `.obj` file that references materials stored in
     * `.mtl` files, p5.js will attempt to load and apply those materials.
     * To ensure that the `.obj` file reads the `.mtl` file correctly include the
     * `.mtl` file alongside it.
     *
     * The first way to call `loadModel()` has three optional parameters after the
     * file path. The first optional parameter, `successCallback`, is a function
     * to call once the model loads. For example,
     * `loadModel('assets/model.obj', handleModel)` will call the `handleModel()`
     * function once the model loads. The second optional parameter,
     * `failureCallback`, is a function to call if the model fails to load. For
     * example, `loadModel('assets/model.obj', handleModel, handleFailure)` will
     * call the `handleFailure()` function if an error occurs while loading. The
     * third optional parameter, `fileType`, is the model’s file extension as a
     * string. For example,
     * `loadModel('assets/model', handleModel, handleFailure, '.obj')` will try to
     * load the file model as a `.obj` file.
     *
     * The second way to call `loadModel()` has four optional parameters after the
     * file path. The first optional parameter is a `Boolean` value. If `true` is
     * passed, as in `loadModel('assets/model.obj', true)`, then the model will be
     * resized to ensure it fits the canvas. The next three parameters are
     * `successCallback`, `failureCallback`, and `fileType` as described above.
     *
     * The third way to call `loadModel()` has one optional parameter after the
     * file path. The optional parameter, `options`, is an `Object` with options,
     * as in `loadModel('assets/model.obj', options)`. The `options` object can
     * have the following properties:
     *
     * ```js
     * let options = {
     *   // Enables standardized size scaling during loading if set to true.
     *   normalize: true,
     *
     *   // Function to call once the model loads.
     *   successCallback: handleModel,
     *
     *   // Function to call if an error occurs while loading.
     *   failureCallback: handleError,
     *
     *   // Model's file extension.
     *   fileType: '.stl',
     *
     *   // Flips the U texture coordinates of the model.
     *   flipU: false,
     *
     *   // Flips the V texture coordinates of the model.
     *   flipV: false
     * };
     *
     * // Pass the options object to loadModel().
     * loadModel('assets/model.obj', options);
     * ```
     *
     * This function returns a `Promise` and should be used in an `async` setup with
     * `await`. See the examples for the usage syntax.
     *
     * Note: There’s no support for colored STL files. STL files with color will
     * be rendered without color.
     *
     * @method loadModel
     * @param  {String|Request} path      path of the model to be loaded.
     * @param  {String} [fileType]          model’s file extension. Either `'.obj'` or `'.stl'`.
     * @param  {Boolean} [normalize]        if `true`, scale the model to fit the canvas.
     * @param  {function(p5.Geometry)} [successCallback] function to call once the model is loaded. Will be passed
     *                                                   the <a href="#/p5.Geometry">p5.Geometry</a> object.
     * @param  {function(Event)} [failureCallback] function to call if the model fails to load. Will be passed an `Error` event object.
     * @return {Promise<p5.Geometry>} the <a href="#/p5.Geometry">p5.Geometry</a> object
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let shape;
     *
     * // Load the file and create a p5.Geometry object.
     * async function setup() {
     *   shape = await loadModel('assets/teapot.obj');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white teapot drawn against a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the shape.
     *   model(shape);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let shape;
     *
     * // Load the file and create a p5.Geometry object.
     * // Normalize the geometry's size to fit the canvas.
     * async function setup() {
     *   shape = await loadModel('assets/teapot.obj', true);
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white teapot drawn against a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the shape.
     *   model(shape);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let shape;
     *
     * // Load the file and create a p5.Geometry object.
     * async function setup() {
     *   await loadModel('assets/teapot.obj', true, handleModel);
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white teapot drawn against a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the shape.
     *   model(shape);
     * }
     *
     * // Set the shape variable and log the geometry's
     * // ID to the console.
     * function handleModel(data) {
     *   shape = data;
     *   console.log(shape.gid);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let shape;
     *
     * // Load the file and create a p5.Geometry object.
     * async function setup() {
     *   await loadModel('assets/teapot.obj', true, handleModel, handleError);
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white teapot drawn against a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the shape.
     *   model(shape);
     * }
     *
     * // Set the shape variable and print the geometry's
     * // ID to the console.
     * function handleModel(data) {
     *   shape = data;
     *   console.log(shape.gid);
     * }
     *
     * // Print an error message if the file doesn't load.
     * function handleError(error) {
     *   console.error('Oops!', error);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let shape;
     *
     * // Load the file and create a p5.Geometry object.
     * async function setup() {
     *   await loadModel('assets/teapot.obj', '.obj', true, handleModel, handleError);
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white teapot drawn against a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the shape.
     *   model(shape);
     * }
     *
     * // Set the shape variable and print the geometry's
     * // ID to the console.
     * function handleModel(data) {
     *   shape = data;
     *   console.log(shape.gid);
     * }
     *
     * // Print an error message if the file doesn't load.
     * function handleError(error) {
     *   console.error('Oops!', error);
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let shape;
     * let options = {
     *   fileType: '.obj',
     *   normalize: true,
     *   successCallback: handleModel,
     *   failureCallback: handleError
     * };
     *
     * // Load the file and create a p5.Geometry object.
     * async function setup() {
     *   await loadModel('assets/teapot.obj', options);
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white teapot drawn against a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the shape.
     *   model(shape);
     * }
     *
     * // Set the shape variable and print the geometry's
     * // ID to the console.
     * function handleModel(data) {
     *   shape = data;
     *   console.log(shape.gid);
     * }
     *
     * // Print an error message if the file doesn't load.
     * function handleError(error) {
     *   console.error('Oops!', error);
     * }
     */
    /**
     * @method loadModel
     * @param  {String|Request} path
     * @param  {String} [fileType]
     * @param  {function(p5.Geometry)} [successCallback]
     * @param  {function(Event)} [failureCallback]
     * @return {Promise<p5.Geometry>} new <a href="#/p5.Geometry">p5.Geometry</a> object.
     */
    /**
     * @method loadModel
     * @param  {String|Request} path
     * @param  {Object} [options] loading options.
     * @param  {String} [options.fileType]
     * @param  {function(p5.Geometry)} [options.successCallback]
     * @param  {function(Event)} [options.failureCallback]
     * @param  {Boolean} [options.normalize]
     * @param  {Boolean} [options.flipU]
     * @param  {Boolean} [options.flipV]
     * @return {Promise<p5.Geometry>} new <a href="#/p5.Geometry">p5.Geometry</a> object.
     */
    fn.loadModel = async function (
      path,
      fileType,
      normalize,
      successCallback,
      failureCallback
    ) {
      // p5._validateParameters('loadModel', arguments);

      let flipU = false;
      let flipV = false;

      if (typeof fileType === 'object') {
        // Passing in options object
        normalize = fileType.normalize || false;
        successCallback = fileType.successCallback;
        failureCallback = fileType.failureCallback;
        fileType = fileType.fileType || fileType;
        flipU = fileType.flipU || false;
        flipV = fileType.flipV || false;

      } else {
        // Passing in individual parameters
        if(typeof arguments[arguments.length-1] === 'function'){
          if(typeof arguments[arguments.length-2] === 'function'){
            successCallback = arguments[arguments.length-2];
            failureCallback = arguments[arguments.length-1];
          }else {
            successCallback = arguments[arguments.length-1];
          }
        }

        if (typeof fileType === 'string') {
          if(typeof normalize !== 'boolean') normalize = false;

        } else if (typeof fileType === 'boolean') {
          normalize = fileType;
          fileType = path.slice(-4);

        } else {
          fileType = path.slice(-4);
          normalize = false;
        }
      }

      if (fileType.toLowerCase() !== '.obj' && fileType.toLowerCase() !== '.stl') {
        fileType = '.obj';
      }

      const model = new Geometry(undefined, undefined, undefined, this._renderer);
      model.gid = `${path}|${normalize}`;

      async function getMaterials(lines) {
        const parsedMaterialPromises = [];

        for (let line of lines) {
          const mtllibMatch = line.match(/^mtllib (.+)/);

          if (mtllibMatch) {
            // Object has material
            let mtlPath = '';
            const mtlFilename = mtllibMatch[1];
            const objPathParts = path.split('/');
            if (objPathParts.length > 1) {
              objPathParts.pop();
              const objFolderPath = objPathParts.join('/');
              mtlPath = objFolderPath + '/' + mtlFilename;
            } else {
              mtlPath = mtlFilename;
            }

            parsedMaterialPromises.push(
              fileExists(mtlPath).then(exists => {
                if (exists) {
                  return parseMtl(mtlPath);
                } else {
                  console.warn(`MTL file not found or error in parsing; proceeding without materials: ${mtlPath}`);
                  return {};

                }
              }).catch(error => {
                console.warn(`Error loading MTL file: ${mtlPath}`, error);
                return {};
              })
            );
          }
        }

        try {
          const parsedMaterials = await Promise.all(parsedMaterialPromises);
          const materials = Object.assign({}, ...parsedMaterials);
          return materials;
        } catch (error) {
          return {};
        }
      }

      try{
        if (fileType.match(/\.stl$/i)) {
          const { data } = await request(path, 'arrayBuffer');
          parseSTL(model, data);

          if (normalize) {
            model.normalize();
          }

          if (flipU) {
            model.flipU();
          }

          if (flipV) {
            model.flipV();
          }
          model._makeTriangleEdges();

          if (successCallback) {
            return successCallback(model);
          } else {
            return model;
          }

        } else if (fileType.match(/\.obj$/i)) {
          const { data } = await request(path, 'text');
          const lines = data.split('\n');

          const parsedMaterials = await getMaterials(lines);
          parseObj(model, lines, parsedMaterials);

          if (normalize) {
            model.normalize();
          }
          if (flipU) {
            model.flipU();
          }
          if (flipV) {
            model.flipV();
          }
          model._makeTriangleEdges();

          if (successCallback) {
            return successCallback(model);
          } else {
            return model;
          }
        }
      } catch(err) {
        p5._friendlyFileLoadError(3, path);
        if(failureCallback) {
          return failureCallback(err);
        } else {
          throw err;
        }
      }
    };

    /**
     * @private
     */
    async function parseMtl(mtlPath) {
      let currentMaterial = null;
      let materials = {};

      const { data } = await request(mtlPath, 'text');
      const lines = data.split('\n');

      for (let line = 0; line < lines.length; ++line) {
        const tokens = lines[line].trim().split(/\s+/);
        if (tokens[0] === 'newmtl') {
          const materialName = tokens[1];
          currentMaterial = materialName;
          materials[currentMaterial] = {};
        } else if (tokens[0] === 'Kd') {
          //Diffuse color
          materials[currentMaterial].diffuseColor = [
            parseFloat(tokens[1]),
            parseFloat(tokens[2]),
            parseFloat(tokens[3])
          ];
        } else if (tokens[0] === 'Ka') {
          //Ambient Color
          materials[currentMaterial].ambientColor = [
            parseFloat(tokens[1]),
            parseFloat(tokens[2]),
            parseFloat(tokens[3])
          ];
        } else if (tokens[0] === 'Ks') {
          //Specular color
          materials[currentMaterial].specularColor = [
            parseFloat(tokens[1]),
            parseFloat(tokens[2]),
            parseFloat(tokens[3])
          ];

        } else if (tokens[0] === 'map_Kd') {
          //Texture path
          materials[currentMaterial].texturePath = tokens[1];
        }
      }

      return materials;
    }

    /**
     * @private
     * Parse OBJ lines into model. For reference, this is what a simple model of a
     * square might look like:
     *
     * v -0.5 -0.5 0.5
     * v -0.5 -0.5 -0.5
     * v -0.5 0.5 -0.5
     * v -0.5 0.5 0.5
     *
     * f 4 3 2 1
     */
    function parseObj(model, lines, materials = {}) {
      // OBJ allows a face to specify an index for a vertex (in the above example),
      // but it also allows you to specify a custom combination of vertex, UV
      // coordinate, and vertex normal. So, "3/4/3" would mean, "use vertex 3 with
      // UV coordinate 4 and vertex normal 3". In WebGL, every vertex with different
      // parameters must be a different vertex, so loadedVerts is used to
      // temporarily store the parsed vertices, normals, etc., and indexedVerts is
      // used to map a specific combination (keyed on, for example, the string
      // "3/4/3"), to the actual index of the newly created vertex in the final
      // object.
      const loadedVerts = {
        v: [],
        vt: [],
        vn: []
      };


      // Map from source index → Map of material → destination index
      const usedVerts = {}; // Track colored vertices
      let currentMaterial = null;
      let hasColoredVertices = false;
      let hasColorlessVertices = false;
      for (let line = 0; line < lines.length; ++line) {
        // Each line is a separate object (vertex, face, vertex normal, etc)
        // For each line, split it into tokens on whitespace. The first token
        // describes the type.
        const tokens = lines[line].trim().split(/\b\s+/);

        if (tokens.length > 0) {
          if (tokens[0] === 'usemtl') {
            // Switch to a new material
            currentMaterial = tokens[1];
          } else if (tokens[0] === 'v' || tokens[0] === 'vn') {
            // Check if this line describes a vertex or vertex normal.
            // It will have three numeric parameters.
            const vertex = new Vector(
              parseFloat(tokens[1]),
              parseFloat(tokens[2]),
              parseFloat(tokens[3])
            );
            loadedVerts[tokens[0]].push(vertex);
          } else if (tokens[0] === 'vt') {
            // Check if this line describes a texture coordinate.
            // It will have two numeric parameters U and V (W is omitted).
            // Because of WebGL texture coordinates rendering behaviour, the V
            // coordinate is inversed.
            const texVertex = [parseFloat(tokens[1]), 1 - parseFloat(tokens[2])];
            loadedVerts[tokens[0]].push(texVertex);
          } else if (tokens[0] === 'f') {
            // Check if this line describes a face.
            // OBJ faces can have more than three points. Triangulate points.
            for (let tri = 3; tri < tokens.length; ++tri) {
              const face = [];
              const vertexTokens = [1, tri - 1, tri];

              for (let tokenInd = 0; tokenInd < vertexTokens.length; ++tokenInd) {
                // Now, convert the given token into an index
                const vertString = tokens[vertexTokens[tokenInd]];
                let vertParts = vertString.split('/');

                // TODO: Faces can technically use negative numbers to refer to the
                // previous nth vertex. I haven't seen this used in practice, but
                // it might be good to implement this in the future.

                for (let i = 0; i < vertParts.length; i++) {
                  vertParts[i] = parseInt(vertParts[i]) - 1;
                }

                if (!usedVerts[vertString]) {
                  usedVerts[vertString] = {};
                }

                if (usedVerts[vertString][currentMaterial] === undefined) {
                  const vertIndex = model.vertices.length;
                  model.vertices.push(loadedVerts.v[vertParts[0]].copy());
                  model.uvs.push(loadedVerts.vt[vertParts[1]] ?
                    loadedVerts.vt[vertParts[1]].slice() : [0, 0]);
                  model.vertexNormals.push(loadedVerts.vn[vertParts[2]] ?
                    loadedVerts.vn[vertParts[2]].copy() : new Vector());

                  usedVerts[vertString][currentMaterial] = vertIndex;
                  face.push(vertIndex);
                  if (currentMaterial
                    && materials[currentMaterial]
                    && materials[currentMaterial].diffuseColor) {
                    hasColoredVertices = true;
                    const materialDiffuseColor =
                      materials[currentMaterial].diffuseColor;
                    model.vertexColors.push(materialDiffuseColor[0]);
                    model.vertexColors.push(materialDiffuseColor[1]);
                    model.vertexColors.push(materialDiffuseColor[2]);
                    model.vertexColors.push(1);
                  } else {
                    hasColorlessVertices = true;
                  }
                } else {
                  face.push(usedVerts[vertString][currentMaterial]);
                }
              }

              if (
                face[0] !== face[1] &&
                face[0] !== face[2] &&
                face[1] !== face[2]
              ) {
                model.faces.push(face);
              }
            }
          }
        }
      }
      // If the model doesn't have normals, compute the normals
      if (model.vertexNormals.length === 0) {
        model.computeNormals();
      }
      if (hasColoredVertices === hasColorlessVertices) {
        // If both are true or both are false, throw an error because the model is inconsistent
        throw new Error('Model coloring is inconsistent. Either all vertices should have colors or none should.');
      }

      return model;
    }

    /**
     * @private
     * STL files can be of two types, ASCII and Binary,
     *
     * We need to convert the arrayBuffer to an array of strings,
     * to parse it as an ASCII file.
     */
    function parseSTL(model, buffer) {
      if (isBinary(buffer)) {
        parseBinarySTL(model, buffer);
      } else {
        const reader = new DataView(buffer);

        if (!('TextDecoder' in window)) {
          console.warn(
            'Sorry, ASCII STL loading only works in browsers that support TextDecoder (https://caniuse.com/#feat=textencoder)'
          );
          return model;
        }

        const decoder = new TextDecoder('utf-8');
        const lines = decoder.decode(reader);
        const lineArray = lines.split('\n');
        parseASCIISTL(model, lineArray);
      }
      return model;
    }

    /**
     * @private
     * This function checks if the file is in ASCII format or in Binary format
     *
     * It is done by searching keyword `solid` at the start of the file.
     *
     * An ASCII STL data must begin with `solid` as the first six bytes.
     * However, ASCII STLs lacking the SPACE after the `d` are known to be
     * plentiful. So, check the first 5 bytes for `solid`.
     *
     * Several encodings, such as UTF-8, precede the text with up to 5 bytes:
     * https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding
     * Search for `solid` to start anywhere after those prefixes.
     */
    function isBinary(data) {
      const reader = new DataView(data);

      // US-ASCII ordinal values for `s`, `o`, `l`, `i`, `d`
      const solid = [115, 111, 108, 105, 100];
      for (let off = 0; off < 5; off++) {
        // If "solid" text is matched to the current offset, declare it to be an ASCII STL.
        if (matchDataViewAt(solid, reader, off)) return false;
      }

      // Couldn't find "solid" text at the beginning; it is binary STL.
      return true;
    }

    /**
     * @private
     * This function matches the `query` at the provided `offset`
     */
    function matchDataViewAt(query, reader, offset) {
      // Check if each byte in query matches the corresponding byte from the current offset
      for (let i = 0, il = query.length; i < il; i++) {
        if (query[i] !== reader.getUint8(offset + i, false)) return false;
      }

      return true;
    }

    /**
     * @private
     * This function parses the Binary STL files.
     * https://en.wikipedia.org/wiki/STL_%28file_format%29#Binary_STL
     *
     * Currently there is no support for the colors provided in STL files.
     */
    function parseBinarySTL(model, buffer) {
      const reader = new DataView(buffer);

      // Number of faces is present following the header
      const faces = reader.getUint32(80, true);
      let r,
        g,
        b,
        hasColors = false,
        colors;
      let defaultR, defaultG, defaultB;

      // Binary files contain 80-byte header, which is generally ignored.
      for (let index = 0; index < 80 - 10; index++) {
        // Check for `COLOR=`
        if (
          reader.getUint32(index, false) === 0x434f4c4f /*COLO*/ &&
          reader.getUint8(index + 4) === 0x52 /*'R'*/ &&
          reader.getUint8(index + 5) === 0x3d /*'='*/
        ) {
          hasColors = true;
          colors = [];

          defaultR = reader.getUint8(index + 6) / 255;
          defaultG = reader.getUint8(index + 7) / 255;
          defaultB = reader.getUint8(index + 8) / 255;
          // To be used when color support is added
          // alpha = reader.getUint8(index + 9) / 255;
        }
      }
      const dataOffset = 84;
      const faceLength = 12 * 4 + 2;

      // Iterate the faces
      for (let face = 0; face < faces; face++) {
        const start = dataOffset + face * faceLength;
        const normalX = reader.getFloat32(start, true);
        const normalY = reader.getFloat32(start + 4, true);
        const normalZ = reader.getFloat32(start + 8, true);

        if (hasColors) {
          const packedColor = reader.getUint16(start + 48, true);

          if ((packedColor & 0x8000) === 0) {
            // facet has its own unique color
            r = (packedColor & 0x1f) / 31;
            g = ((packedColor >> 5) & 0x1f) / 31;
            b = ((packedColor >> 10) & 0x1f) / 31;
          } else {
            r = defaultR;
            g = defaultG;
            b = defaultB;
          }
        }
        const newNormal = new Vector(normalX, normalY, normalZ);

        for (let i = 1; i <= 3; i++) {
          const vertexstart = start + i * 12;

          const newVertex = new Vector(
            reader.getFloat32(vertexstart, true),
            reader.getFloat32(vertexstart + 4, true),
            reader.getFloat32(vertexstart + 8, true)
          );

          model.vertices.push(newVertex);
          model.vertexNormals.push(newNormal);

          if (hasColors) {
            colors.push(r, g, b);
          }
        }

        model.faces.push([3 * face, 3 * face + 1, 3 * face + 2]);
        model.uvs.push([0, 0], [0, 0], [0, 0]);
      }
      return model;
    }

    /**
     * @private
     * ASCII STL file starts with `solid 'nameOfFile'`
     * Then contain the normal of the face, starting with `facet normal`
     * Next contain a keyword indicating the start of face vertex, `outer loop`
     * Next comes the three vertex, starting with `vertex x y z`
     * Vertices ends with `endloop`
     * Face ends with `endfacet`
     * Next face starts with `facet normal`
     * The end of the file is indicated by `endsolid`
     */
    function parseASCIISTL(model, lines) {
      let state = '';
      let curVertexIndex = [];
      let newNormal, newVertex;

      for (let iterator = 0; iterator < lines.length; ++iterator) {
        const line = lines[iterator].trim();
        const parts = line.split(' ');

        for (
          let partsiterator = 0;
          partsiterator < parts.length;
          ++partsiterator
        ) {
          if (parts[partsiterator] === '') {
            // Ignoring multiple whitespaces
            parts.splice(partsiterator, 1);
          }
        }

        if (parts.length === 0) {
          // Remove newline
          continue;
        }

        switch (state) {
          case '': // First run
            if (parts[0] !== 'solid') {
              // Invalid state
              console.error(line);
              console.error(`Invalid state "${parts[0]}", should be "solid"`);
              return;
            } else {
              state = 'solid';
            }
            break;

          case 'solid': // First face
            if (parts[0] !== 'facet' || parts[1] !== 'normal') {
              // Invalid state
              console.error(line);
              console.error(
                `Invalid state "${parts[0]}", should be "facet normal"`
              );
              return;
            } else {
              // Push normal for first face
              newNormal = new Vector(
                parseFloat(parts[2]),
                parseFloat(parts[3]),
                parseFloat(parts[4])
              );
              model.vertexNormals.push(newNormal, newNormal, newNormal);
              state = 'facet normal';
            }
            break;

          case 'facet normal': // After normal is defined
            if (parts[0] !== 'outer' || parts[1] !== 'loop') {
              // Invalid State
              console.error(line);
              console.error(`Invalid state "${parts[0]}", should be "outer loop"`);
              return;
            } else {
              // Next should be vertices
              state = 'vertex';
            }
            break;

          case 'vertex':
            if (parts[0] === 'vertex') {
              //Vertex of triangle
              newVertex = new Vector(
                parseFloat(parts[1]),
                parseFloat(parts[2]),
                parseFloat(parts[3])
              );
              model.vertices.push(newVertex);
              model.uvs.push([0, 0]);
              curVertexIndex.push(model.vertices.indexOf(newVertex));
            } else if (parts[0] === 'endloop') {
              // End of vertices
              model.faces.push(curVertexIndex);
              curVertexIndex = [];
              state = 'endloop';
            } else {
              // Invalid State
              console.error(line);
              console.error(
                `Invalid state "${parts[0]}", should be "vertex" or "endloop"`
              );
              return;
            }
            break;

          case 'endloop':
            if (parts[0] !== 'endfacet') {
              // End of face
              console.error(line);
              console.error(`Invalid state "${parts[0]}", should be "endfacet"`);
              return;
            } else {
              state = 'endfacet';
            }
            break;

          case 'endfacet':
            if (parts[0] === 'endsolid') ; else if (parts[0] === 'facet' && parts[1] === 'normal') {
              // Next face
              newNormal = new Vector(
                parseFloat(parts[2]),
                parseFloat(parts[3]),
                parseFloat(parts[4])
              );
              model.vertexNormals.push(newNormal, newNormal, newNormal);
              state = 'facet normal';
            } else {
              // Invalid State
              console.error(line);
              console.error(
                `Invalid state "${parts[0]
              }", should be "endsolid" or "facet normal"`
              );
              return;
            }
            break;

          default:
            console.error(`Invalid state "${state}"`);
            break;
        }
      }
      return model;
    }

    /**
     * Draws a <a href="#/p5.Geometry">p5.Geometry</a> object to the canvas.
     *
     * The parameter, `model`, is the
     * <a href="#/p5.Geometry">p5.Geometry</a> object to draw.
     * <a href="#/p5.Geometry">p5.Geometry</a> objects can be built with
     * <a href="#/p5/buildGeometry">buildGeometry()</a>, or
     * <a href="#/p5/beginGeometry">beginGeometry()</a> and
     * <a href="#/p5/endGeometry">endGeometry()</a>. They can also be loaded from
     * a file with <a href="#/p5/loadGeometry">loadGeometry()</a>.
     *
     * Note: `model()` can only be used in WebGL mode.
     *
     * @method model
     * @param  {p5.Geometry} model 3D shape to be drawn.
     *
     * @param {Number} [count=1] number of instances to draw.
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let shape;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the p5.Geometry object.
     *   shape = buildGeometry(createShape);
     *
     *   describe('A white cone drawn on a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the p5.Geometry object.
     *   model(shape);
     * }
     *
     * // Create p5.Geometry object from a single cone.
     * function createShape() {
     *   cone();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let shape;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *
     *   // Create the p5.Geometry object.
     *   shape = buildGeometry(createArrow);
     *
     *   describe('Two white arrows drawn on a gray background. The arrow on the right rotates slowly.');
     * }
     *
     * function draw() {
     *   background(50);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Turn on the lights.
     *   lights();
     *
     *   // Style the arrows.
     *   noStroke();
     *
     *   // Draw the p5.Geometry object.
     *   model(shape);
     *
     *   // Translate and rotate the coordinate system.
     *   translate(30, 0, 0);
     *   rotateZ(frameCount * 0.01);
     *
     *   // Draw the p5.Geometry object again.
     *   model(shape);
     * }
     *
     * function createArrow() {
     *   // Add shapes to the p5.Geometry object.
     *   push();
     *   rotateX(PI);
     *   cone(10);
     *   translate(0, -10, 0);
     *   cylinder(3, 20);
     *   pop();
     * }
     *
     * @example
     * // Click and drag the mouse to view the scene from different angles.
     *
     * let shape;
     *
     * async function setup() {
     *   shape = await loadModel('assets/octahedron.obj');
     *
     *   createCanvas(100, 100, WEBGL);
     *
     *   describe('A white octahedron drawn against a gray background.');
     * }
     *
     * function draw() {
     *   background(200);
     *
     *   // Enable orbiting with the mouse.
     *   orbitControl();
     *
     *   // Draw the shape.
     *   model(shape);
     * }
     */
    fn.model = function (model, count = 1) {
      this._assert3d('model');
      // p5._validateParameters('model', arguments);
      this._renderer.model(model, count);
    };

    /**
     * Load a 3d model from an OBJ or STL string.
     *
     * OBJ and STL files lack a built-in sense of scale, causing models exported from different programs to vary in size.
     * If your model doesn't display correctly, consider using `loadModel()` with `normalize` set to `true` to standardize its size.
     * Further adjustments can be made using the `scale()` function.
     *
     * Also, the support for colored STL files is not present. STL files with color will be
     * rendered without color properties.
     *
     * * Options can include:
     * - `modelString`: Specifies the plain text string of either an stl or obj file to be loaded.
     * - `fileType`: Defines the file extension of the model.
     * - `normalize`: Enables standardized size scaling during loading if set to true.
     * - `successCallback`: Callback for post-loading actions with the 3D model object.
     * - `failureCallback`: Handles errors if model loading fails, receiving an event error.
     * - `flipU`: Flips the U texture coordinates of the model.
     * - `flipV`: Flips the V texture coordinates of the model.
     *
     *
     * @method createModel
     * @param  {String} modelString         String of the object to be loaded
     * @param  {String} [fileType]          The file extension of the model
     *                                      (<code>.stl</code>, <code>.obj</code>).
     * @param  {Boolean} [normalize]        If true, scale the model to a
     *                                      standardized size when loading
     * @param  {function(p5.Geometry)} [successCallback] Function to be called
     *                                     once the model is loaded. Will be passed
     *                                     the 3D model object.
     * @param  {function(Event)} [failureCallback] called with event error if
     *                                         the model fails to load.
     * @return {p5.Geometry} the <a href="#/p5.Geometry">p5.Geometry</a> object
     *
     * @example
     * const octahedron_model = `
     * v 0.000000E+00 0.000000E+00 40.0000
     * v 22.5000 22.5000 0.000000E+00
     * v 22.5000 -22.5000 0.000000E+00
     * v -22.5000 -22.5000 0.000000E+00
     * v -22.5000 22.5000 0.000000E+00
     * v 0.000000E+00 0.000000E+00 -40.0000
     * f     1 2 3
     * f     1 3 4
     * f     1 4 5
     * f     1 5 2
     * f     6 5 4
     * f     6 4 3
     * f     6 3 2
     * f     6 2 5
     * `;
     * //draw a spinning octahedron
     * let octahedron;
     *
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *   octahedron = createModel(octahedron_model, '.obj');
     *   describe('Vertically rotating 3D octahedron.');
     * }
     *
     * function draw() {
     *   background(200);
     *   rotateX(frameCount * 0.01);
     *   rotateY(frameCount * 0.01);
     *   model(octahedron);
     * }
     */
    /**
     * @method createModel
     * @param  {String} modelString
     * @param  {String} [fileType]
     * @param  {function(p5.Geometry)} [successCallback]
     * @param  {function(Event)} [failureCallback]
     * @return {p5.Geometry} the <a href="#/p5.Geometry">p5.Geometry</a> object
     */
    /**
     * @method createModel
     * @param  {String} modelString
     * @param  {String} [fileType]
     * @param  {Object} [options]
     * @param  {function(p5.Geometry)} [options.successCallback]
     * @param  {function(Event)} [options.failureCallback]
     * @param  {boolean} [options.normalize]
     * @param  {boolean} [options.flipU]
     * @param  {boolean} [options.flipV]
     * @return {p5.Geometry} the <a href="#/p5.Geometry">p5.Geometry</a> object
     */
    let modelCounter = 0;
    fn.createModel = function(modelString, fileType=' ', options) {
      // p5._validateParameters('createModel', arguments);
      let normalize= false;
      let successCallback;
      let failureCallback;
      let flipU = false;
      let flipV = false;
      if (options && typeof options === 'object') {
        normalize = options.normalize || false;
        successCallback = options.successCallback;
        failureCallback = options.failureCallback;
        flipU = options.flipU || false;
        flipV = options.flipV || false;
      } else if (typeof options === 'boolean') {
        normalize = options;
        successCallback = arguments[3];
        failureCallback = arguments[4];
      } else {
        successCallback = typeof arguments[2] === 'function' ? arguments[2] : undefined;
        failureCallback = arguments[3];
      }
      const model = new p5.Geometry();
      model.gid = `${fileType}|${normalize}|${modelCounter++}`;

      if (fileType.match(/\.stl$/i)) {
        try {
          let uint8array = new TextEncoder().encode(modelString);
          let arrayBuffer = uint8array.buffer;
          parseSTL(model, arrayBuffer);
        } catch (error) {
          if (failureCallback) {
            failureCallback(error);
          } else {
            p5._friendlyError('Error during parsing: ' + error.message);
          }
          return;
        }
      } else if (fileType.match(/\.obj$/i)) {
        try {
          const lines = modelString.split('\n');
          parseObj(model, lines);
        } catch (error) {
          if (failureCallback) {
            failureCallback(error);
          } else {
            p5._friendlyError('Error during parsing: ' + error.message);
          }
          return;
        }
      } else {
        p5._friendlyFileLoadError(3, modelString);
        if (failureCallback) {
          failureCallback();
        } else {
          p5._friendlyError(
            'Sorry, the file type is invalid. Only OBJ and STL files are supported.'
          );
        }
      }
      if (normalize) {
        model.normalize();
      }

      if (flipU) {
        model.flipU();
      }

      if (flipV) {
        model.flipV();
      }

      model._makeTriangleEdges();

      if (typeof successCallback === 'function') {
        successCallback(model);
      }

      return model;
    };
  }

  if(typeof p5 !== 'undefined'){
    loading$1(p5, p5.prototype);
  }

  var lib = {};

  Object.defineProperty(lib, "__esModule", { value: true });
  var UnicodeRange = /** @class */ (function () {
      function UnicodeRange() {
      }
      UnicodeRange.parse = function (arr) {
          var result = new Set();
          for (var _i = 0, arr_1 = arr; _i < arr_1.length; _i++) {
              var range = arr_1[_i];
              if (!UnicodeRange.REGEXP.test(range)) {
                  throw new TypeError("\"" + range + "\" is invalid unicode-range.");
              }
              var _a = range.match(UnicodeRange.REGEXP), single = _a[1], start = _a[2], end = _a[3];
              // Single unicode-range (e.g. U+20, U+3F U+30??)
              if (single) {
                  if (/\?[^?]+$/.test(single)) {
                      throw new TypeError("\"" + range + "\" is invalid unicode-range.");
                  }
                  if (single.includes('?')) {
                      var start_1 = single.replace(/\?/g, '0');
                      var end_1 = single.replace(/\?/g, 'F');
                      var tmp = UnicodeRange.parse(["U+" + start_1 + "-" + end_1]);
                      for (var _b = 0, tmp_1 = tmp; _b < tmp_1.length; _b++) {
                          var codePoint = tmp_1[_b];
                          result.add(codePoint);
                      }
                  }
                  else {
                      result.add(parseInt(single, 16));
                  }
              }
              // Interval unicode-range (e.g. U+30-39)
              if (start && end) {
                  var startCodePoint = parseInt(start, 16);
                  var endCodePoint = parseInt(end, 16);
                  for (var codePoint = startCodePoint; codePoint <= endCodePoint; codePoint++) {
                      result.add(codePoint);
                  }
              }
          }
          return Array.from(result).sort(function (a, b) { return a - b; });
      };
      UnicodeRange.stringify = function (arr) {
          var sorted = Array.from(new Set(arr)).sort(function (a, b) { return a - b; });
          var results = [];
          var rangeStart;
          for (var idx = 0; idx < sorted.length; idx++) {
              var current = sorted[idx];
              var prev = sorted[idx - 1];
              if (rangeStart && current - prev !== 1) {
                  results.push(UnicodeRange.rangeString(rangeStart, prev));
                  rangeStart = current;
              }
              // First
              if (!rangeStart) {
                  rangeStart = current;
              }
              // Last
              if (idx === sorted.length - 1) {
                  if (rangeStart === current) {
                      results.push(UnicodeRange.rangeString(current));
                  }
                  else {
                      results.push(UnicodeRange.rangeString(rangeStart, current));
                  }
              }
          }
          return results;
      };
      UnicodeRange.rangeString = function (start, end) {
          if (!end || start === end) {
              return "U+" + start.toString(16);
          }
          return "U+" + start.toString(16) + "-" + end.toString(16);
      };
      UnicodeRange.REGEXP = /^u\+(?:([0-9a-f]?[0-9a-f?]{1,5})|([0-9a-f]{1,6})-([0-9a-f]{1,6}))?$/i;
      return UnicodeRange;
  }());
  var UnicodeRange_1 = lib.UnicodeRange = UnicodeRange;

  // From https://github.com/radiovisual/unicode-range-json,
  // with decimal ranges removed to save space, and with category
  // names converted to lowercase ahead of time
  const unicodeRanges = [
    {
      category: 'control character',
      hexrange: ['0000', '001f']
    },
    {
      category: 'basic latin',
      hexrange: ['0000', '007f']
    },
    {
      category: 'latin-1 supplement',
      hexrange: ['0080', '00ff']
    },
    {
      category: 'latin extended-a',
      hexrange: ['0100', '017f']
    },
    {
      category: 'latin extended-b',
      hexrange: ['0180', '024f']
    },
    {
      category: 'ipa extensions',
      hexrange: ['0250', '02af']
    },
    {
      category: 'spacing modifier letters',
      hexrange: ['02b0', '02ff']
    },
    {
      category: 'combining diacritical marks',
      hexrange: ['0300', '036f']
    },
    {
      category: 'greek and coptic',
      hexrange: ['0370', '03ff']
    },
    {
      category: 'cyrillic',
      hexrange: ['0400', '04ff']
    },
    {
      category: 'cyrillic supplement',
      hexrange: ['0500', '052f']
    },
    {
      category: 'armenian',
      hexrange: ['0530', '058f']
    },
    {
      category: 'hebrew',
      hexrange: ['0590', '05ff']
    },
    {
      category: 'arabic',
      hexrange: ['0600', '06ff']
    },
    {
      category: 'syriac',
      hexrange: ['0700', '074f']
    },
    {
      category: 'arabic supplement',
      hexrange: ['0750', '077f']
    },
    {
      category: 'thaana',
      hexrange: ['0780', '07bf']
    },
    {
      category: 'nko',
      hexrange: ['07c0', '07ff']
    },
    {
      category: 'samaritan',
      hexrange: ['0800', '083f']
    },
    {
      category: 'mandaic',
      hexrange: ['0840', '085f']
    },
    {
      category: 'syriac supplement',
      hexrange: ['0860', '086f']
    },
    {
      category: 'arabic extended-b',
      hexrange: ['0870', '089f']
    },
    {
      category: 'arabic extended-a',
      hexrange: ['08a0', '08ff']
    },
    {
      category: 'devanagari',
      hexrange: ['0900', '097f']
    },
    {
      category: 'bengali',
      hexrange: ['0980', '09ff']
    },
    {
      category: 'gurmukhi',
      hexrange: ['0a00', '0a7f']
    },
    {
      category: 'gujarati',
      hexrange: ['0a80', '0aff']
    },
    {
      category: 'oriya',
      hexrange: ['0b00', '0b7f']
    },
    {
      category: 'tamil',
      hexrange: ['0b80', '0bff']
    },
    {
      category: 'telugu',
      hexrange: ['0c00', '0c7f']
    },
    {
      category: 'kannada',
      hexrange: ['0c80', '0cff']
    },
    {
      category: 'malayalam',
      hexrange: ['0d00', '0d7f']
    },
    {
      category: 'sinhala',
      hexrange: ['0d80', '0dff']
    },
    {
      category: 'thai',
      hexrange: ['0e00', '0e7f']
    },
    {
      category: 'lao',
      hexrange: ['0e80', '0eff']
    },
    {
      category: 'tibetan',
      hexrange: ['0f00', '0fff']
    },
    {
      category: 'myanmar',
      hexrange: ['1000', '109f']
    },
    {
      category: 'georgian',
      hexrange: ['10a0', '10ff']
    },
    {
      category: 'hangul jamo',
      hexrange: ['1100', '11ff']
    },
    {
      category: 'ethiopic',
      hexrange: ['1200', '137f']
    },
    {
      category: 'ethiopic supplement',
      hexrange: ['1380', '139f']
    },
    {
      category: 'cherokee',
      hexrange: ['13a0', '13ff']
    },
    {
      category: 'unified canadian aboriginal syllabics',
      hexrange: ['1400', '167f']
    },
    {
      category: 'ogham',
      hexrange: ['1680', '169f']
    },
    {
      category: 'runic',
      hexrange: ['16a0', '16ff']
    },
    {
      category: 'tagalog',
      hexrange: ['1700', '171f']
    },
    {
      category: 'hanunoo',
      hexrange: ['1720', '173f']
    },
    {
      category: 'buhid',
      hexrange: ['1740', '175f']
    },
    {
      category: 'tagbanwa',
      hexrange: ['1760', '177f']
    },
    {
      category: 'khmer',
      hexrange: ['1780', '17ff']
    },
    {
      category: 'mongolian',
      hexrange: ['1800', '18af']
    },
    {
      category: 'unified canadian aboriginal syllabics extended',
      hexrange: ['18b0', '18ff']
    },
    {
      category: 'limbu',
      hexrange: ['1900', '194f']
    },
    {
      category: 'tai le',
      hexrange: ['1950', '197f']
    },
    {
      category: 'new tai lue',
      hexrange: ['1980', '19df']
    },
    {
      category: 'khmer symbols',
      hexrange: ['19e0', '19ff']
    },
    {
      category: 'buginese',
      hexrange: ['1a00', '1a1f']
    },
    {
      category: 'tai tham',
      hexrange: ['1a20', '1aaf']
    },
    {
      category: 'combining diacritical marks extended',
      hexrange: ['1ab0', '1aff']
    },
    {
      category: 'balinese',
      hexrange: ['1b00', '1b7f']
    },
    {
      category: 'sundanese',
      hexrange: ['1b80', '1bbf']
    },
    {
      category: 'batak',
      hexrange: ['1bc0', '1bff']
    },
    {
      category: 'lepcha',
      hexrange: ['1c00', '1c4f']
    },
    {
      category: 'ol chiki',
      hexrange: ['1c50', '1c7f']
    },
    {
      category: 'cyrillic extended-c',
      hexrange: ['1c80', '1c8f']
    },
    {
      category: 'georgian extended',
      hexrange: ['1c90', '1cbf']
    },
    {
      category: 'sundanese supplement',
      hexrange: ['1cc0', '1ccf']
    },
    {
      category: 'vedic extensions',
      hexrange: ['1cd0', '1cff']
    },
    {
      category: 'phonetic extensions',
      hexrange: ['1d00', '1d7f']
    },
    {
      category: 'phonetic extensions supplement',
      hexrange: ['1d80', '1dbf']
    },
    {
      category: 'combining diacritical marks supplement',
      hexrange: ['1dc0', '1dff']
    },
    {
      category: 'latin extended additional',
      hexrange: ['1e00', '1eff']
    },
    {
      category: 'greek extended',
      hexrange: ['1f00', '1fff']
    },
    {
      category: 'general punctuation',
      hexrange: ['2000', '206f']
    },
    {
      category: 'superscripts and subscripts',
      hexrange: ['2070', '209f']
    },
    {
      category: 'currency symbols',
      hexrange: ['20a0', '20cf']
    },
    {
      category: 'combining diacritical marks for symbols',
      hexrange: ['20d0', '20ff']
    },
    {
      category: 'letterlike symbols',
      hexrange: ['2100', '214f']
    },
    {
      category: 'number forms',
      hexrange: ['2150', '218f']
    },
    {
      category: 'arrows',
      hexrange: ['2190', '21ff']
    },
    {
      category: 'mathematical operators',
      hexrange: ['2200', '22ff']
    },
    {
      category: 'miscellaneous technical',
      hexrange: ['2300', '23ff']
    },
    {
      category: 'control pictures',
      hexrange: ['2400', '243f']
    },
    {
      category: 'optical character recognition',
      hexrange: ['2440', '245f']
    },
    {
      category: 'enclosed alphanumerics',
      hexrange: ['2460', '24ff']
    },
    {
      category: 'box drawing',
      hexrange: ['2500', '257f']
    },
    {
      category: 'block elements',
      hexrange: ['2580', '259f']
    },
    {
      category: 'geometric shapes',
      hexrange: ['25a0', '25ff']
    },
    {
      category: 'miscellaneous symbols',
      hexrange: ['2600', '26ff']
    },
    {
      category: 'dingbats',
      hexrange: ['2700', '27bf']
    },
    {
      category: 'miscellaneous mathematical symbols-a',
      hexrange: ['27c0', '27ef']
    },
    {
      category: 'supplemental arrows-a',
      hexrange: ['27f0', '27ff']
    },
    {
      category: 'braille patterns',
      hexrange: ['2800', '28ff']
    },
    {
      category: 'supplemental arrows-b',
      hexrange: ['2900', '297f']
    },
    {
      category: 'miscellaneous mathematical symbols-b',
      hexrange: ['2980', '29ff']
    },
    {
      category: 'supplemental mathematical operators',
      hexrange: ['2a00', '2aff']
    },
    {
      category: 'miscellaneous symbols and arrows',
      hexrange: ['2b00', '2bff']
    },
    {
      category: 'glagolitic',
      hexrange: ['2c00', '2c5f']
    },
    {
      category: 'latin extended-c',
      hexrange: ['2c60', '2c7f']
    },
    {
      category: 'coptic',
      hexrange: ['2c80', '2cff']
    },
    {
      category: 'georgian supplement',
      hexrange: ['2d00', '2d2f']
    },
    {
      category: 'tifinagh',
      hexrange: ['2d30', '2d7f']
    },
    {
      category: 'ethiopic extended',
      hexrange: ['2d80', '2ddf']
    },
    {
      category: 'cyrillic extended-a',
      hexrange: ['2de0', '2dff']
    },
    {
      category: 'supplemental punctuation',
      hexrange: ['2e00', '2e7f']
    },
    {
      category: 'cjk radicals supplement',
      hexrange: ['2e80', '2eff']
    },
    {
      category: 'kangxi radicals',
      hexrange: ['2f00', '2fdf']
    },
    {
      category: 'ideographic description characters',
      hexrange: ['2ff0', '2fff']
    },
    {
      category: 'cjk symbols and punctuation',
      hexrange: ['3000', '303f']
    },
    {
      category: 'hiragana',
      hexrange: ['3040', '309f']
    },
    {
      category: 'katakana',
      hexrange: ['30a0', '30ff']
    },
    {
      category: 'bopomofo',
      hexrange: ['3100', '312f']
    },
    {
      category: 'hangul compatibility jamo',
      hexrange: ['3130', '318f']
    },
    {
      category: 'kanbun',
      hexrange: ['3190', '319f']
    },
    {
      category: 'bopomofo extended',
      hexrange: ['31a0', '31bf']
    },
    {
      category: 'cjk strokes',
      hexrange: ['31c0', '31ef']
    },
    {
      category: 'katakana phonetic extensions',
      hexrange: ['31f0', '31ff']
    },
    {
      category: 'enclosed cjk letters and months',
      hexrange: ['3200', '32ff']
    },
    {
      category: 'cjk compatibility',
      hexrange: ['3300', '33ff']
    },
    {
      category: 'cjk unified ideographs extension a',
      hexrange: ['3400', '4dbf']
    },
    {
      category: 'yijing hexagram symbols',
      hexrange: ['4dc0', '4dff']
    },
    {
      category: 'cjk unified ideographs',
      hexrange: ['4e00', '9fff']
    },
    {
      category: 'yi syllables',
      hexrange: ['a000', 'a48f']
    },
    {
      category: 'yi radicals',
      hexrange: ['a490', 'a4cf']
    },
    {
      category: 'lisu',
      hexrange: ['a4d0', 'a4ff']
    },
    {
      category: 'vai',
      hexrange: ['a500', 'a63f']
    },
    {
      category: 'cyrillic extended-b',
      hexrange: ['a640', 'a69f']
    },
    {
      category: 'bamum',
      hexrange: ['a6a0', 'a6ff']
    },
    {
      category: 'modifier tone letters',
      hexrange: ['a700', 'a71f']
    },
    {
      category: 'latin extended-d',
      hexrange: ['a720', 'a7ff']
    },
    {
      category: 'syloti nagri',
      hexrange: ['a800', 'a82f']
    },
    {
      category: 'common indic number forms',
      hexrange: ['a830', 'a83f']
    },
    {
      category: 'phags-pa',
      hexrange: ['a840', 'a87f']
    },
    {
      category: 'saurashtra',
      hexrange: ['a880', 'a8df']
    },
    {
      category: 'devanagari extended',
      hexrange: ['a8e0', 'a8ff']
    },
    {
      category: 'kayah li',
      hexrange: ['a900', 'a92f']
    },
    {
      category: 'rejang',
      hexrange: ['a930', 'a95f']
    },
    {
      category: 'hangul jamo extended-a',
      hexrange: ['a960', 'a97f']
    },
    {
      category: 'javanese',
      hexrange: ['a980', 'a9df']
    },
    {
      category: 'myanmar extended-b',
      hexrange: ['a9e0', 'a9ff']
    },
    {
      category: 'cham',
      hexrange: ['aa00', 'aa5f']
    },
    {
      category: 'myanmar extended-a',
      hexrange: ['aa60', 'aa7f']
    },
    {
      category: 'tai viet',
      hexrange: ['aa80', 'aadf']
    },
    {
      category: 'meetei mayek extensions',
      hexrange: ['aae0', 'aaff']
    },
    {
      category: 'ethiopic extended-a',
      hexrange: ['ab00', 'ab2f']
    },
    {
      category: 'latin extended-e',
      hexrange: ['ab30', 'ab6f']
    },
    {
      category: 'cherokee supplement',
      hexrange: ['ab70', 'abbf']
    },
    {
      category: 'meetei mayek',
      hexrange: ['abc0', 'abff']
    },
    {
      category: 'hangul syllables',
      hexrange: ['ac00', 'd7af']
    },
    {
      category: 'hangul jamo extended-b',
      hexrange: ['d7b0', 'd7ff']
    },
    {
      category: 'high surrogates',
      hexrange: ['d800', 'db7f']
    },
    {
      category: 'high private use surrogates',
      hexrange: ['db80', 'dbff']
    },
    {
      category: 'low surrogates',
      hexrange: ['dc00', 'dfff']
    },
    {
      category: 'private use area',
      hexrange: ['e000', 'f8ff']
    },
    {
      category: 'cjk compatibility ideographs',
      hexrange: ['f900', 'faff']
    },
    {
      category: 'alphabetic presentation forms',
      hexrange: ['fb00', 'fb4f']
    },
    {
      category: 'arabic presentation forms-a',
      hexrange: ['fb50', 'fdff']
    },
    {
      category: 'variation selectors',
      hexrange: ['fe00', 'fe0f']
    },
    {
      category: 'vertical forms',
      hexrange: ['fe10', 'fe1f']
    },
    {
      category: 'combining half marks',
      hexrange: ['fe20', 'fe2f']
    },
    {
      category: 'cjk compatibility forms',
      hexrange: ['fe30', 'fe4f']
    },
    {
      category: 'small form variants',
      hexrange: ['fe50', 'fe6f']
    },
    {
      category: 'arabic presentation forms-b',
      hexrange: ['fe70', 'feff']
    },
    {
      category: 'halfwidth and fullwidth forms',
      hexrange: ['ff00', 'ffef']
    },
    {
      category: 'specials',
      hexrange: ['fff0', 'ffff']
    },
    {
      category: 'linear b syllabary',
      hexrange: ['10000', '1007f']
    },
    {
      category: 'linear b ideograms',
      hexrange: ['10080', '100ff']
    },
    {
      category: 'aegean numbers',
      hexrange: ['10100', '1013f']
    },
    {
      category: 'ancient greek numbers',
      hexrange: ['10140', '1018f']
    },
    {
      category: 'ancient symbols',
      hexrange: ['10190', '101cf']
    },
    {
      category: 'phaistos disc',
      hexrange: ['101d0', '101ff']
    },
    {
      category: 'lycian',
      hexrange: ['10280', '1029f']
    },
    {
      category: 'carian',
      hexrange: ['102a0', '102df']
    },
    {
      category: 'coptic epact numbers',
      hexrange: ['102e0', '102ff']
    },
    {
      category: 'old italic',
      hexrange: ['10300', '1032f']
    },
    {
      category: 'gothic',
      hexrange: ['10330', '1034f']
    },
    {
      category: 'old permic',
      hexrange: ['10350', '1037f']
    },
    {
      category: 'ugaritic',
      hexrange: ['10380', '1039f']
    },
    {
      category: 'old persian',
      hexrange: ['103a0', '103df']
    },
    {
      category: 'deseret',
      hexrange: ['10400', '1044f']
    },
    {
      category: 'shavian',
      hexrange: ['10450', '1047f']
    },
    {
      category: 'osmanya',
      hexrange: ['10480', '104af']
    },
    {
      category: 'osage',
      hexrange: ['104b0', '104ff']
    },
    {
      category: 'elbasan',
      hexrange: ['10500', '1052f']
    },
    {
      category: 'caucasian albanian',
      hexrange: ['10530', '1056f']
    },
    {
      category: 'vithkuqi',
      hexrange: ['10570', '105bf']
    },
    {
      category: 'linear a',
      hexrange: ['10600', '1077f']
    },
    {
      category: 'latin extended-f',
      hexrange: ['10780', '107bf']
    },
    {
      category: 'cypriot syllabary',
      hexrange: ['10800', '1083f']
    },
    {
      category: 'imperial aramaic',
      hexrange: ['10840', '1085f']
    },
    {
      category: 'palmyrene',
      hexrange: ['10860', '1087f']
    },
    {
      category: 'nabataean',
      hexrange: ['10880', '108af']
    },
    {
      category: 'hatran',
      hexrange: ['108e0', '108ff']
    },
    {
      category: 'phoenician',
      hexrange: ['10900', '1091f']
    },
    {
      category: 'lydian',
      hexrange: ['10920', '1093f']
    },
    {
      category: 'meroitic hieroglyphs',
      hexrange: ['10980', '1099f']
    },
    {
      category: 'meroitic cursive',
      hexrange: ['109a0', '109ff']
    },
    {
      category: 'kharoshthi',
      hexrange: ['10a00', '10a5f']
    },
    {
      category: 'old south arabian',
      hexrange: ['10a60', '10a7f']
    },
    {
      category: 'old north arabian',
      hexrange: ['10a80', '10a9f']
    },
    {
      category: 'manichaean',
      hexrange: ['10ac0', '10aff']
    },
    {
      category: 'avestan',
      hexrange: ['10b00', '10b3f']
    },
    {
      category: 'inscriptional parthian',
      hexrange: ['10b40', '10b5f']
    },
    {
      category: 'inscriptional pahlavi',
      hexrange: ['10b60', '10b7f']
    },
    {
      category: 'psalter pahlavi',
      hexrange: ['10b80', '10baf']
    },
    {
      category: 'old turkic',
      hexrange: ['10c00', '10c4f']
    },
    {
      category: 'old hungarian',
      hexrange: ['10c80', '10cff']
    },
    {
      category: 'hanifi rohingya',
      hexrange: ['10d00', '10d3f']
    },
    {
      category: 'rumi numeral symbols',
      hexrange: ['10e60', '10e7f']
    },
    {
      category: 'yezidi',
      hexrange: ['10e80', '10ebf']
    },
    {
      category: 'arabic extended-c',
      hexrange: ['10ec0', '10eff']
    },
    {
      category: 'old sogdian',
      hexrange: ['10f00', '10f2f']
    },
    {
      category: 'sogdian',
      hexrange: ['10f30', '10f6f']
    },
    {
      category: 'old uyghur',
      hexrange: ['10f70', '10faf']
    },
    {
      category: 'chorasmian',
      hexrange: ['10fb0', '10fdf']
    },
    {
      category: 'elymaic',
      hexrange: ['10fe0', '10fff']
    },
    {
      category: 'brahmi',
      hexrange: ['11000', '1107f']
    },
    {
      category: 'kaithi',
      hexrange: ['11080', '110cf']
    },
    {
      category: 'sora sompeng',
      hexrange: ['110d0', '110ff']
    },
    {
      category: 'chakma',
      hexrange: ['11100', '1114f']
    },
    {
      category: 'mahajani',
      hexrange: ['11150', '1117f']
    },
    {
      category: 'sharada',
      hexrange: ['11180', '111df']
    },
    {
      category: 'sinhala archaic numbers',
      hexrange: ['111e0', '111ff']
    },
    {
      category: 'khojki',
      hexrange: ['11200', '1124f']
    },
    {
      category: 'multani',
      hexrange: ['11280', '112af']
    },
    {
      category: 'khudawadi',
      hexrange: ['112b0', '112ff']
    },
    {
      category: 'grantha',
      hexrange: ['11300', '1137f']
    },
    {
      category: 'newa',
      hexrange: ['11400', '1147f']
    },
    {
      category: 'tirhuta',
      hexrange: ['11480', '114df']
    },
    {
      category: 'siddham',
      hexrange: ['11580', '115ff']
    },
    {
      category: 'modi',
      hexrange: ['11600', '1165f']
    },
    {
      category: 'mongolian supplement',
      hexrange: ['11660', '1167f']
    },
    {
      category: 'takri',
      hexrange: ['11680', '116cf']
    },
    {
      category: 'ahom',
      hexrange: ['11700', '1174f']
    },
    {
      category: 'dogra',
      hexrange: ['11800', '1184f']
    },
    {
      category: 'warang citi',
      hexrange: ['118a0', '118ff']
    },
    {
      category: 'dives akuru',
      hexrange: ['11900', '1195f']
    },
    {
      category: 'nandinagari',
      hexrange: ['119a0', '119ff']
    },
    {
      category: 'zanabazar square',
      hexrange: ['11a00', '11a4f']
    },
    {
      category: 'soyombo',
      hexrange: ['11a50', '11aaf']
    },
    {
      category: 'unified canadian aboriginal syllabics extended-a',
      hexrange: ['11ab0', '11abf']
    },
    {
      category: 'pau cin hau',
      hexrange: ['11ac0', '11aff']
    },
    {
      category: 'devanagari extended-a',
      hexrange: ['11b00', '11b5f']
    },
    {
      category: 'bhaiksuki',
      hexrange: ['11c00', '11c6f']
    },
    {
      category: 'marchen',
      hexrange: ['11c70', '11cbf']
    },
    {
      category: 'masaram gondi',
      hexrange: ['11d00', '11d5f']
    },
    {
      category: 'gunjala gondi',
      hexrange: ['11d60', '11daf']
    },
    {
      category: 'makasar',
      hexrange: ['11ee0', '11eff']
    },
    {
      category: 'kawi',
      hexrange: ['11f00', '11f5f']
    },
    {
      category: 'lisu supplement',
      hexrange: ['11fb0', '11fbf']
    },
    {
      category: 'tamil supplement',
      hexrange: ['11fc0', '11fff']
    },
    {
      category: 'cuneiform',
      hexrange: ['12000', '123ff']
    },
    {
      category: 'cuneiform numbers and punctuation',
      hexrange: ['12400', '1247f']
    },
    {
      category: 'early dynastic cuneiform',
      hexrange: ['12480', '1254f']
    },
    {
      category: 'cypro-minoan',
      hexrange: ['12f90', '12fff']
    },
    {
      category: 'egyptian hieroglyphs',
      hexrange: ['13000', '1342f']
    },
    {
      category: 'egyptian hieroglyph format controls',
      hexrange: ['13430', '1345f']
    },
    {
      category: 'anatolian hieroglyphs',
      hexrange: ['14400', '1467f']
    },
    {
      category: 'bamum supplement',
      hexrange: ['16800', '16a3f']
    },
    {
      category: 'mro',
      hexrange: ['16a40', '16a6f']
    },
    {
      category: 'tangsa',
      hexrange: ['16a70', '16acf']
    },
    {
      category: 'bassa vah',
      hexrange: ['16ad0', '16aff']
    },
    {
      category: 'pahawh hmong',
      hexrange: ['16b00', '16b8f']
    },
    {
      category: 'medefaidrin',
      hexrange: ['16e40', '16e9f']
    },
    {
      category: 'miao',
      hexrange: ['16f00', '16f9f']
    },
    {
      category: 'ideographic symbols and punctuation',
      hexrange: ['16fe0', '16fff']
    },
    {
      category: 'tangut',
      hexrange: ['17000', '187ff']
    },
    {
      category: 'tangut components',
      hexrange: ['18800', '18aff']
    },
    {
      category: 'khitan small script',
      hexrange: ['18b00', '18cff']
    },
    {
      category: 'tangut supplement',
      hexrange: ['18d00', '18d7f']
    },
    {
      category: 'kana extended-b',
      hexrange: ['1aff0', '1afff']
    },
    {
      category: 'kana supplement',
      hexrange: ['1b000', '1b0ff']
    },
    {
      category: 'kana extended-a',
      hexrange: ['1b100', '1b12f']
    },
    {
      category: 'small kana extension',
      hexrange: ['1b130', '1b16f']
    },
    {
      category: 'nushu',
      hexrange: ['1b170', '1b2ff']
    },
    {
      category: 'duployan',
      hexrange: ['1bc00', '1bc9f']
    },
    {
      category: 'shorthand format controls',
      hexrange: ['1bca0', '1bcaf']
    },
    {
      category: 'znamenny musical notation',
      hexrange: ['1cf00', '1cfcf']
    },
    {
      category: 'byzantine musical symbols',
      hexrange: ['1d000', '1d0ff']
    },
    {
      category: 'musical symbols',
      hexrange: ['1d100', '1d1ff']
    },
    {
      category: 'ancient greek musical notation',
      hexrange: ['1d200', '1d24f']
    },
    {
      category: 'kaktovik numerals',
      hexrange: ['1d2c0', '1d2df']
    },
    {
      category: 'mayan numerals',
      hexrange: ['1d2e0', '1d2ff']
    },
    {
      category: 'tai xuan jing symbols',
      hexrange: ['1d300', '1d35f']
    },
    {
      category: 'counting rod numerals',
      hexrange: ['1d360', '1d37f']
    },
    {
      category: 'mathematical alphanumeric symbols',
      hexrange: ['1d400', '1d7ff']
    },
    {
      category: 'sutton signwriting',
      hexrange: ['1d800', '1daaf']
    },
    {
      category: 'latin extended-g',
      hexrange: ['1df00', '1dfff']
    },
    {
      category: 'glagolitic supplement',
      hexrange: ['1e000', '1e02f']
    },
    {
      category: 'cyrillic extended-d',
      hexrange: ['1e030', '1e08f']
    },
    {
      category: 'nyiakeng puachue hmong',
      hexrange: ['1e100', '1e14f']
    },
    {
      category: 'toto',
      hexrange: ['1e290', '1e2bf']
    },
    {
      category: 'wancho',
      hexrange: ['1e2c0', '1e2ff']
    },
    {
      category: 'nag mundari',
      hexrange: ['1e4d0', '1e4ff']
    },
    {
      category: 'ethiopic extended-b',
      hexrange: ['1e7e0', '1e7ff']
    },
    {
      category: 'mende kikakui',
      hexrange: ['1e800', '1e8df']
    },
    {
      category: 'adlam',
      hexrange: ['1e900', '1e95f']
    },
    {
      category: 'indic siyaq numbers',
      hexrange: ['1ec70', '1ecbf']
    },
    {
      category: 'ottoman siyaq numbers',
      hexrange: ['1ed00', '1ed4f']
    },
    {
      category: 'arabic mathematical alphabetic symbols',
      hexrange: ['1ee00', '1eeff']
    },
    {
      category: 'mahjong tiles',
      hexrange: ['1f000', '1f02f']
    },
    {
      category: 'domino tiles',
      hexrange: ['1f030', '1f09f']
    },
    {
      category: 'playing cards',
      hexrange: ['1f0a0', '1f0ff']
    },
    {
      category: 'enclosed alphanumeric supplement',
      hexrange: ['1f100', '1f1ff']
    },
    {
      category: 'enclosed ideographic supplement',
      hexrange: ['1f200', '1f2ff']
    },
    {
      category: 'miscellaneous symbols and pictographs',
      hexrange: ['1f300', '1f5ff']
    },
    {
      category: 'emoticons (emoji)',
      hexrange: ['1f600', '1f64f']
    },
    {
      category: 'ornamental dingbats',
      hexrange: ['1f650', '1f67f']
    },
    {
      category: 'transport and map symbols',
      hexrange: ['1f680', '1f6ff']
    },
    {
      category: 'alchemical symbols',
      hexrange: ['1f700', '1f77f']
    },
    {
      category: 'geometric shapes extended',
      hexrange: ['1f780', '1f7ff']
    },
    {
      category: 'supplemental arrows-c',
      hexrange: ['1f800', '1f8ff']
    },
    {
      category: 'supplemental symbols and pictographs',
      hexrange: ['1f900', '1f9ff']
    },
    {
      category: 'chess symbols',
      hexrange: ['1fa00', '1fa6f']
    },
    {
      category: 'symbols and pictographs extended-a',
      hexrange: ['1fa70', '1faff']
    },
    {
      category: 'symbols for legacy computing',
      hexrange: ['1fb00', '1fbff']
    },
    {
      category: 'cjk unified ideographs extension b',
      hexrange: ['20000', '2a6df']
    },
    {
      category: 'cjk unified ideographs extension c',
      hexrange: ['2a700', '2b73f']
    },
    {
      category: 'cjk unified ideographs extension d',
      hexrange: ['2b740', '2b81f']
    },
    {
      category: 'cjk unified ideographs extension e',
      hexrange: ['2b820', '2ceaf']
    },
    {
      category: 'cjk unified ideographs extension f',
      hexrange: ['2ceb0', '2ebef']
    },
    {
      category: 'cjk unified ideographs extension i',
      hexrange: ['2ebf0', '2ee5f']
    },
    {
      category: 'cjk compatibility ideographs supplement',
      hexrange: ['2f800', '2fa1f']
    },
    {
      category: 'cjk unified ideographs extension g',
      hexrange: ['30000', '3134f']
    },
    {
      category: 'cjk unified ideographs extension h',
      hexrange: ['31350', '323af']
    },
    {
      category: 'tags',
      hexrange: ['e0000', 'e007f']
    },
    {
      category: 'variation selectors supplement',
      hexrange: ['e0100', 'e01ef']
    },
    {
      category: 'supplementary private use area-a',
      hexrange: ['f0000', 'fffff']
    },
    {
      category: 'supplementary private use area-b',
      hexrange: ['100000', '10ffff']
    }
  ];

  /*============================================================================*/


  function zero$1(buf) { let len = buf.length; while (--len >= 0) { buf[len] = 0; } }
  /* The three kinds of block type */

  const MIN_MATCH$1    = 3;
  const MAX_MATCH$1    = 258;
  /* The minimum and maximum match lengths */

  // From deflate.h
  /* ===========================================================================
   * Internal compression state.
   */

  const LENGTH_CODES$1  = 29;
  /* number of length codes, not counting the special END_BLOCK code */

  const LITERALS$1      = 256;
  /* number of literal bytes 0..255 */

  const L_CODES$1       = LITERALS$1 + 1 + LENGTH_CODES$1;
  /* number of Literal or Length codes, including the END_BLOCK code */

  const D_CODES$1       = 30;
  /* eslint-enable comma-spacing,array-bracket-spacing */

  /* The lengths of the bit length codes are sent in order of decreasing
   * probability, to avoid transmitting the lengths for unused bit length codes.
   */

  /* ===========================================================================
   * Local data. These are initialized only once.
   */

  // We pre-fill arrays with 0 to avoid uninitialized gaps

  const DIST_CODE_LEN = 512; /* see definition of array dist_code below */

  // !!!! Use flat array instead of structure, Freq = i*2, Len = i*2+1
  const static_ltree  = new Array((L_CODES$1 + 2) * 2);
  zero$1(static_ltree);
  /* The static literal tree. Since the bit lengths are imposed, there is no
   * need for the L_CODES extra codes used during heap construction. However
   * The codes 286 and 287 are needed to build a canonical tree (see _tr_init
   * below).
   */

  const static_dtree  = new Array(D_CODES$1 * 2);
  zero$1(static_dtree);
  /* The static distance tree. (Actually a trivial tree since all codes use
   * 5 bits.)
   */

  const _dist_code    = new Array(DIST_CODE_LEN);
  zero$1(_dist_code);
  /* Distance codes. The first 256 values correspond to the distances
   * 3 .. 258, the last 256 values correspond to the top 8 bits of
   * the 15 bit distances.
   */

  const _length_code  = new Array(MAX_MATCH$1 - MIN_MATCH$1 + 1);
  zero$1(_length_code);
  /* length code for each normalized match length (0 == MIN_MATCH) */

  const base_length   = new Array(LENGTH_CODES$1);
  zero$1(base_length);
  /* First normalized length for each code (0 = MIN_MATCH) */

  const base_dist     = new Array(D_CODES$1);
  zero$1(base_dist);

  // Note: adler32 takes 12% for level 0 and 2% for level 6.
  // It isn't worth it to make additional optimizations as in original.
  // Small size is preferable.

  // (C) 1995-2013 Jean-loup Gailly and Mark Adler
  // (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
  //
  // This software is provided 'as-is', without any express or implied
  // warranty. In no event will the authors be held liable for any damages
  // arising from the use of this software.
  //
  // Permission is granted to anyone to use this software for any purpose,
  // including commercial applications, and to alter it and redistribute it
  // freely, subject to the following restrictions:
  //
  // 1. The origin of this software must not be misrepresented; you must not
  //   claim that you wrote the original software. If you use this software
  //   in a product, an acknowledgment in the product documentation would be
  //   appreciated but is not required.
  // 2. Altered source versions must be plainly marked as such, and must not be
  //   misrepresented as being the original software.
  // 3. This notice may not be removed or altered from any source distribution.

  const adler32 = (adler, buf, len, pos) => {
    let s1 = (adler & 0xffff) |0,
        s2 = ((adler >>> 16) & 0xffff) |0,
        n = 0;

    while (len !== 0) {
      // Set limit ~ twice less than 5552, to keep
      // s2 in 31-bits, because we force signed ints.
      // in other case %= will fail.
      n = len > 2000 ? 2000 : len;
      len -= n;

      do {
        s1 = (s1 + buf[pos++]) |0;
        s2 = (s2 + s1) |0;
      } while (--n);

      s1 %= 65521;
      s2 %= 65521;
    }

    return (s1 | (s2 << 16)) |0;
  };


  var adler32_1 = adler32;

  // Note: we can't get significant speed boost here.
  // So write code to minimize size - no pregenerated tables
  // and array tools dependencies.

  // (C) 1995-2013 Jean-loup Gailly and Mark Adler
  // (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
  //
  // This software is provided 'as-is', without any express or implied
  // warranty. In no event will the authors be held liable for any damages
  // arising from the use of this software.
  //
  // Permission is granted to anyone to use this software for any purpose,
  // including commercial applications, and to alter it and redistribute it
  // freely, subject to the following restrictions:
  //
  // 1. The origin of this software must not be misrepresented; you must not
  //   claim that you wrote the original software. If you use this software
  //   in a product, an acknowledgment in the product documentation would be
  //   appreciated but is not required.
  // 2. Altered source versions must be plainly marked as such, and must not be
  //   misrepresented as being the original software.
  // 3. This notice may not be removed or altered from any source distribution.

  // Use ordinary array, since untyped makes no boost here
  const makeTable = () => {
    let c, table = [];

    for (var n = 0; n < 256; n++) {
      c = n;
      for (var k = 0; k < 8; k++) {
        c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
      }
      table[n] = c;
    }

    return table;
  };

  // Create table on load. Just 255 signed longs. Not a problem.
  const crcTable = new Uint32Array(makeTable());


  const crc32 = (crc, buf, len, pos) => {
    const t = crcTable;
    const end = pos + len;

    crc ^= -1;

    for (let i = pos; i < end; i++) {
      crc = (crc >>> 8) ^ t[(crc ^ buf[i]) & 0xFF];
    }

    return (crc ^ (-1)); // >>> 0;
  };


  var crc32_1 = crc32;

  // (C) 1995-2013 Jean-loup Gailly and Mark Adler
  // (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
  //
  // This software is provided 'as-is', without any express or implied
  // warranty. In no event will the authors be held liable for any damages
  // arising from the use of this software.
  //
  // Permission is granted to anyone to use this software for any purpose,
  // including commercial applications, and to alter it and redistribute it
  // freely, subject to the following restrictions:
  //
  // 1. The origin of this software must not be misrepresented; you must not
  //   claim that you wrote the original software. If you use this software
  //   in a product, an acknowledgment in the product documentation would be
  //   appreciated but is not required.
  // 2. Altered source versions must be plainly marked as such, and must not be
  //   misrepresented as being the original software.
  // 3. This notice may not be removed or altered from any source distribution.

  var messages = {
    2:      'need dictionary',     /* Z_NEED_DICT       2  */
    1:      'stream end',          /* Z_STREAM_END      1  */
    0:      '',                    /* Z_OK              0  */
    '-1':   'file error',          /* Z_ERRNO         (-1) */
    '-2':   'stream error',        /* Z_STREAM_ERROR  (-2) */
    '-3':   'data error',          /* Z_DATA_ERROR    (-3) */
    '-4':   'insufficient memory', /* Z_MEM_ERROR     (-4) */
    '-5':   'buffer error',        /* Z_BUF_ERROR     (-5) */
    '-6':   'incompatible version' /* Z_VERSION_ERROR (-6) */
  };

  // (C) 1995-2013 Jean-loup Gailly and Mark Adler
  // (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
  //
  // This software is provided 'as-is', without any express or implied
  // warranty. In no event will the authors be held liable for any damages
  // arising from the use of this software.
  //
  // Permission is granted to anyone to use this software for any purpose,
  // including commercial applications, and to alter it and redistribute it
  // freely, subject to the following restrictions:
  //
  // 1. The origin of this software must not be misrepresented; you must not
  //   claim that you wrote the original software. If you use this software
  //   in a product, an acknowledgment in the product documentation would be
  //   appreciated but is not required.
  // 2. Altered source versions must be plainly marked as such, and must not be
  //   misrepresented as being the original software.
  // 3. This notice may not be removed or altered from any source distribution.

  var constants$2 = {

    /* Allowed flush values; see deflate() and inflate() below for details */
    Z_NO_FLUSH:         0,
    Z_FINISH:           4,
    Z_BLOCK:            5,
    Z_TREES:            6,

    /* Return codes for the compression/decompression functions. Negative values
    * are errors, positive values are used for special but normal events.
    */
    Z_OK:               0,
    Z_STREAM_END:       1,
    Z_NEED_DICT:        2,
    Z_STREAM_ERROR:    -2,
    Z_DATA_ERROR:      -3,
    Z_MEM_ERROR:       -4,
    Z_BUF_ERROR:       -5,
    /* The deflate compression method */
    Z_DEFLATED:               8
    //Z_NULL:                 null // Use -1 or null inline, depending on var type
  };

  const _has = (obj, key) => {
    return Object.prototype.hasOwnProperty.call(obj, key);
  };

  var assign = function (obj /*from1, from2, from3, ...*/) {
    const sources = Array.prototype.slice.call(arguments, 1);
    while (sources.length) {
      const source = sources.shift();
      if (!source) { continue; }

      if (typeof source !== 'object') {
        throw new TypeError(source + 'must be non-object');
      }

      for (const p in source) {
        if (_has(source, p)) {
          obj[p] = source[p];
        }
      }
    }

    return obj;
  };


  // Join array of chunks to single array.
  var flattenChunks = (chunks) => {
    // calculate data length
    let len = 0;

    for (let i = 0, l = chunks.length; i < l; i++) {
      len += chunks[i].length;
    }

    // join chunks
    const result = new Uint8Array(len);

    for (let i = 0, pos = 0, l = chunks.length; i < l; i++) {
      let chunk = chunks[i];
      result.set(chunk, pos);
      pos += chunk.length;
    }

    return result;
  };

  var common = {
  	assign: assign,
  	flattenChunks: flattenChunks
  };

  // String encode/decode helpers


  // Quick check if we can use fast array to bin string conversion
  //
  // - apply(Array) can fail on Android 2.2
  // - apply(Uint8Array) can fail on iOS 5.1 Safari
  //
  let STR_APPLY_UIA_OK = true;

  try { String.fromCharCode.apply(null, new Uint8Array(1)); } catch (__) { STR_APPLY_UIA_OK = false; }


  // Table with utf8 lengths (calculated by first byte of sequence)
  // Note, that 5 & 6-byte values and some 4-byte values can not be represented in JS,
  // because max possible codepoint is 0x10ffff
  const _utf8len = new Uint8Array(256);
  for (let q = 0; q < 256; q++) {
    _utf8len[q] = (q >= 252 ? 6 : q >= 248 ? 5 : q >= 240 ? 4 : q >= 224 ? 3 : q >= 192 ? 2 : 1);
  }
  _utf8len[254] = _utf8len[254] = 1; // Invalid sequence start


  // convert string to array (typed, when possible)
  var string2buf = (str) => {
    if (typeof TextEncoder === 'function' && TextEncoder.prototype.encode) {
      return new TextEncoder().encode(str);
    }

    let buf, c, c2, m_pos, i, str_len = str.length, buf_len = 0;

    // count binary size
    for (m_pos = 0; m_pos < str_len; m_pos++) {
      c = str.charCodeAt(m_pos);
      if ((c & 0xfc00) === 0xd800 && (m_pos + 1 < str_len)) {
        c2 = str.charCodeAt(m_pos + 1);
        if ((c2 & 0xfc00) === 0xdc00) {
          c = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00);
          m_pos++;
        }
      }
      buf_len += c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0x10000 ? 3 : 4;
    }

    // allocate buffer
    buf = new Uint8Array(buf_len);

    // convert
    for (i = 0, m_pos = 0; i < buf_len; m_pos++) {
      c = str.charCodeAt(m_pos);
      if ((c & 0xfc00) === 0xd800 && (m_pos + 1 < str_len)) {
        c2 = str.charCodeAt(m_pos + 1);
        if ((c2 & 0xfc00) === 0xdc00) {
          c = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00);
          m_pos++;
        }
      }
      if (c < 0x80) {
        /* one byte */
        buf[i++] = c;
      } else if (c < 0x800) {
        /* two bytes */
        buf[i++] = 0xC0 | (c >>> 6);
        buf[i++] = 0x80 | (c & 0x3f);
      } else if (c < 0x10000) {
        /* three bytes */
        buf[i++] = 0xE0 | (c >>> 12);
        buf[i++] = 0x80 | (c >>> 6 & 0x3f);
        buf[i++] = 0x80 | (c & 0x3f);
      } else {
        /* four bytes */
        buf[i++] = 0xf0 | (c >>> 18);
        buf[i++] = 0x80 | (c >>> 12 & 0x3f);
        buf[i++] = 0x80 | (c >>> 6 & 0x3f);
        buf[i++] = 0x80 | (c & 0x3f);
      }
    }

    return buf;
  };

  // Helper
  const buf2binstring = (buf, len) => {
    // On Chrome, the arguments in a function call that are allowed is `65534`.
    // If the length of the buffer is smaller than that, we can use this optimization,
    // otherwise we will take a slower path.
    if (len < 65534) {
      if (buf.subarray && STR_APPLY_UIA_OK) {
        return String.fromCharCode.apply(null, buf.length === len ? buf : buf.subarray(0, len));
      }
    }

    let result = '';
    for (let i = 0; i < len; i++) {
      result += String.fromCharCode(buf[i]);
    }
    return result;
  };


  // convert array to string
  var buf2string = (buf, max) => {
    const len = max || buf.length;

    if (typeof TextDecoder === 'function' && TextDecoder.prototype.decode) {
      return new TextDecoder().decode(buf.subarray(0, max));
    }

    let i, out;

    // Reserve max possible length (2 words per char)
    // NB: by unknown reasons, Array is significantly faster for
    //     String.fromCharCode.apply than Uint16Array.
    const utf16buf = new Array(len * 2);

    for (out = 0, i = 0; i < len;) {
      let c = buf[i++];
      // quick process ascii
      if (c < 0x80) { utf16buf[out++] = c; continue; }

      let c_len = _utf8len[c];
      // skip 5 & 6 byte codes
      if (c_len > 4) { utf16buf[out++] = 0xfffd; i += c_len - 1; continue; }

      // apply mask on first byte
      c &= c_len === 2 ? 0x1f : c_len === 3 ? 0x0f : 0x07;
      // join the rest
      while (c_len > 1 && i < len) {
        c = (c << 6) | (buf[i++] & 0x3f);
        c_len--;
      }

      // terminated by end of string?
      if (c_len > 1) { utf16buf[out++] = 0xfffd; continue; }

      if (c < 0x10000) {
        utf16buf[out++] = c;
      } else {
        c -= 0x10000;
        utf16buf[out++] = 0xd800 | ((c >> 10) & 0x3ff);
        utf16buf[out++] = 0xdc00 | (c & 0x3ff);
      }
    }

    return buf2binstring(utf16buf, out);
  };


  // Calculate max possible position in utf8 buffer,
  // that will not break sequence. If that's not possible
  // - (very small limits) return max size as is.
  //
  // buf[] - utf8 bytes array
  // max   - length limit (mandatory);
  var utf8border = (buf, max) => {

    max = max || buf.length;
    if (max > buf.length) { max = buf.length; }

    // go back from last position, until start of sequence found
    let pos = max - 1;
    while (pos >= 0 && (buf[pos] & 0xC0) === 0x80) { pos--; }

    // Very small and broken sequence,
    // return max, because we should return something anyway.
    if (pos < 0) { return max; }

    // If we came to start of buffer - that means buffer is too small,
    // return max too.
    if (pos === 0) { return max; }

    return (pos + _utf8len[buf[pos]] > max) ? pos : max;
  };

  var strings = {
  	string2buf: string2buf,
  	buf2string: buf2string,
  	utf8border: utf8border
  };

  // (C) 1995-2013 Jean-loup Gailly and Mark Adler
  // (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
  //
  // This software is provided 'as-is', without any express or implied
  // warranty. In no event will the authors be held liable for any damages
  // arising from the use of this software.
  //
  // Permission is granted to anyone to use this software for any purpose,
  // including commercial applications, and to alter it and redistribute it
  // freely, subject to the following restrictions:
  //
  // 1. The origin of this software must not be misrepresented; you must not
  //   claim that you wrote the original software. If you use this software
  //   in a product, an acknowledgment in the product documentation would be
  //   appreciated but is not required.
  // 2. Altered source versions must be plainly marked as such, and must not be
  //   misrepresented as being the original software.
  // 3. This notice may not be removed or altered from any source distribution.

  function ZStream() {
    /* next input byte */
    this.input = null; // JS specific, because we have no pointers
    this.next_in = 0;
    /* number of bytes available at input */
    this.avail_in = 0;
    /* total number of input bytes read so far */
    this.total_in = 0;
    /* next output byte should be put there */
    this.output = null; // JS specific, because we have no pointers
    this.next_out = 0;
    /* remaining free space at output */
    this.avail_out = 0;
    /* total number of bytes output so far */
    this.total_out = 0;
    /* last error message, NULL if no error */
    this.msg = ''/*Z_NULL*/;
    /* not visible by applications */
    this.state = null;
    /* best guess about the data type: binary or text */
    this.data_type = 2/*Z_UNKNOWN*/;
    /* adler32 value of the uncompressed data */
    this.adler = 0;
  }

  var zstream = ZStream;

  // (C) 1995-2013 Jean-loup Gailly and Mark Adler
  // (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
  //
  // This software is provided 'as-is', without any express or implied
  // warranty. In no event will the authors be held liable for any damages
  // arising from the use of this software.
  //
  // Permission is granted to anyone to use this software for any purpose,
  // including commercial applications, and to alter it and redistribute it
  // freely, subject to the following restrictions:
  //
  // 1. The origin of this software must not be misrepresented; you must not
  //   claim that you wrote the original software. If you use this software
  //   in a product, an acknowledgment in the product documentation would be
  //   appreciated but is not required.
  // 2. Altered source versions must be plainly marked as such, and must not be
  //   misrepresented as being the original software.
  // 3. This notice may not be removed or altered from any source distribution.

  // See state defs from inflate.js
  const BAD$1 = 16209;       /* got a data error -- remain here until reset */
  const TYPE$1 = 16191;      /* i: waiting for type bits, including last-flag bit */

  /*
     Decode literal, length, and distance codes and write out the resulting
     literal and match bytes until either not enough input or output is
     available, an end-of-block is encountered, or a data error is encountered.
     When large enough input and output buffers are supplied to inflate(), for
     example, a 16K input buffer and a 64K output buffer, more than 95% of the
     inflate execution time is spent in this routine.

     Entry assumptions:

          state.mode === LEN
          strm.avail_in >= 6
          strm.avail_out >= 258
          start >= strm.avail_out
          state.bits < 8

     On return, state.mode is one of:

          LEN -- ran out of enough output space or enough available input
          TYPE -- reached end of block code, inflate() to interpret next block
          BAD -- error in block data

     Notes:

      - The maximum input bits used by a length/distance pair is 15 bits for the
        length code, 5 bits for the length extra, 15 bits for the distance code,
        and 13 bits for the distance extra.  This totals 48 bits, or six bytes.
        Therefore if strm.avail_in >= 6, then there is enough input to avoid
        checking for available input while decoding.

      - The maximum bytes that a single length/distance pair can output is 258
        bytes, which is the maximum length that can be coded.  inflate_fast()
        requires strm.avail_out >= 258 for each loop to avoid checking for
        output space.
   */
  var inffast = function inflate_fast(strm, start) {
    let _in;                    /* local strm.input */
    let last;                   /* have enough input while in < last */
    let _out;                   /* local strm.output */
    let beg;                    /* inflate()'s initial strm.output */
    let end;                    /* while out < end, enough space available */
  //#ifdef INFLATE_STRICT
    let dmax;                   /* maximum distance from zlib header */
  //#endif
    let wsize;                  /* window size or zero if not using window */
    let whave;                  /* valid bytes in the window */
    let wnext;                  /* window write index */
    // Use `s_window` instead `window`, avoid conflict with instrumentation tools
    let s_window;               /* allocated sliding window, if wsize != 0 */
    let hold;                   /* local strm.hold */
    let bits;                   /* local strm.bits */
    let lcode;                  /* local strm.lencode */
    let dcode;                  /* local strm.distcode */
    let lmask;                  /* mask for first level of length codes */
    let dmask;                  /* mask for first level of distance codes */
    let here;                   /* retrieved table entry */
    let op;                     /* code bits, operation, extra bits, or */
                                /*  window position, window bytes to copy */
    let len;                    /* match length, unused bytes */
    let dist;                   /* match distance */
    let from;                   /* where to copy match from */
    let from_source;


    let input, output; // JS specific, because we have no pointers

    /* copy state to local variables */
    const state = strm.state;
    //here = state.here;
    _in = strm.next_in;
    input = strm.input;
    last = _in + (strm.avail_in - 5);
    _out = strm.next_out;
    output = strm.output;
    beg = _out - (start - strm.avail_out);
    end = _out + (strm.avail_out - 257);
  //#ifdef INFLATE_STRICT
    dmax = state.dmax;
  //#endif
    wsize = state.wsize;
    whave = state.whave;
    wnext = state.wnext;
    s_window = state.window;
    hold = state.hold;
    bits = state.bits;
    lcode = state.lencode;
    dcode = state.distcode;
    lmask = (1 << state.lenbits) - 1;
    dmask = (1 << state.distbits) - 1;


    /* decode literals and length/distances until end-of-block or not enough
       input data or output space */

    top:
    do {
      if (bits < 15) {
        hold += input[_in++] << bits;
        bits += 8;
        hold += input[_in++] << bits;
        bits += 8;
      }

      here = lcode[hold & lmask];

      dolen:
      for (;;) { // Goto emulation
        op = here >>> 24/*here.bits*/;
        hold >>>= op;
        bits -= op;
        op = (here >>> 16) & 0xff/*here.op*/;
        if (op === 0) {                          /* literal */
          //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ?
          //        "inflate:         literal '%c'\n" :
          //        "inflate:         literal 0x%02x\n", here.val));
          output[_out++] = here & 0xffff/*here.val*/;
        }
        else if (op & 16) {                     /* length base */
          len = here & 0xffff/*here.val*/;
          op &= 15;                           /* number of extra bits */
          if (op) {
            if (bits < op) {
              hold += input[_in++] << bits;
              bits += 8;
            }
            len += hold & ((1 << op) - 1);
            hold >>>= op;
            bits -= op;
          }
          //Tracevv((stderr, "inflate:         length %u\n", len));
          if (bits < 15) {
            hold += input[_in++] << bits;
            bits += 8;
            hold += input[_in++] << bits;
            bits += 8;
          }
          here = dcode[hold & dmask];

          dodist:
          for (;;) { // goto emulation
            op = here >>> 24/*here.bits*/;
            hold >>>= op;
            bits -= op;
            op = (here >>> 16) & 0xff/*here.op*/;

            if (op & 16) {                      /* distance base */
              dist = here & 0xffff/*here.val*/;
              op &= 15;                       /* number of extra bits */
              if (bits < op) {
                hold += input[_in++] << bits;
                bits += 8;
                if (bits < op) {
                  hold += input[_in++] << bits;
                  bits += 8;
                }
              }
              dist += hold & ((1 << op) - 1);
  //#ifdef INFLATE_STRICT
              if (dist > dmax) {
                strm.msg = 'invalid distance too far back';
                state.mode = BAD$1;
                break top;
              }
  //#endif
              hold >>>= op;
              bits -= op;
              //Tracevv((stderr, "inflate:         distance %u\n", dist));
              op = _out - beg;                /* max distance in output */
              if (dist > op) {                /* see if copy from window */
                op = dist - op;               /* distance back in window */
                if (op > whave) {
                  if (state.sane) {
                    strm.msg = 'invalid distance too far back';
                    state.mode = BAD$1;
                    break top;
                  }

  // (!) This block is disabled in zlib defaults,
  // don't enable it for binary compatibility
  //#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR
  //                if (len <= op - whave) {
  //                  do {
  //                    output[_out++] = 0;
  //                  } while (--len);
  //                  continue top;
  //                }
  //                len -= op - whave;
  //                do {
  //                  output[_out++] = 0;
  //                } while (--op > whave);
  //                if (op === 0) {
  //                  from = _out - dist;
  //                  do {
  //                    output[_out++] = output[from++];
  //                  } while (--len);
  //                  continue top;
  //                }
  //#endif
                }
                from = 0; // window index
                from_source = s_window;
                if (wnext === 0) {           /* very common case */
                  from += wsize - op;
                  if (op < len) {         /* some from window */
                    len -= op;
                    do {
                      output[_out++] = s_window[from++];
                    } while (--op);
                    from = _out - dist;  /* rest from output */
                    from_source = output;
                  }
                }
                else if (wnext < op) {      /* wrap around window */
                  from += wsize + wnext - op;
                  op -= wnext;
                  if (op < len) {         /* some from end of window */
                    len -= op;
                    do {
                      output[_out++] = s_window[from++];
                    } while (--op);
                    from = 0;
                    if (wnext < len) {  /* some from start of window */
                      op = wnext;
                      len -= op;
                      do {
                        output[_out++] = s_window[from++];
                      } while (--op);
                      from = _out - dist;      /* rest from output */
                      from_source = output;
                    }
                  }
                }
                else {                      /* contiguous in window */
                  from += wnext - op;
                  if (op < len) {         /* some from window */
                    len -= op;
                    do {
                      output[_out++] = s_window[from++];
                    } while (--op);
                    from = _out - dist;  /* rest from output */
                    from_source = output;
                  }
                }
                while (len > 2) {
                  output[_out++] = from_source[from++];
                  output[_out++] = from_source[from++];
                  output[_out++] = from_source[from++];
                  len -= 3;
                }
                if (len) {
                  output[_out++] = from_source[from++];
                  if (len > 1) {
                    output[_out++] = from_source[from++];
                  }
                }
              }
              else {
                from = _out - dist;          /* copy direct from output */
                do {                        /* minimum length is three */
                  output[_out++] = output[from++];
                  output[_out++] = output[from++];
                  output[_out++] = output[from++];
                  len -= 3;
                } while (len > 2);
                if (len) {
                  output[_out++] = output[from++];
                  if (len > 1) {
                    output[_out++] = output[from++];
                  }
                }
              }
            }
            else if ((op & 64) === 0) {          /* 2nd level distance code */
              here = dcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];
              continue dodist;
            }
            else {
              strm.msg = 'invalid distance code';
              state.mode = BAD$1;
              break top;
            }

            break; // need to emulate goto via "continue"
          }
        }
        else if ((op & 64) === 0) {              /* 2nd level length code */
          here = lcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];
          continue dolen;
        }
        else if (op & 32) {                     /* end-of-block */
          //Tracevv((stderr, "inflate:         end of block\n"));
          state.mode = TYPE$1;
          break top;
        }
        else {
          strm.msg = 'invalid literal/length code';
          state.mode = BAD$1;
          break top;
        }

        break; // need to emulate goto via "continue"
      }
    } while (_in < last && _out < end);

    /* return unused bytes (on entry, bits < 8, so in won't go too far back) */
    len = bits >> 3;
    _in -= len;
    bits -= len << 3;
    hold &= (1 << bits) - 1;

    /* update state and return */
    strm.next_in = _in;
    strm.next_out = _out;
    strm.avail_in = (_in < last ? 5 + (last - _in) : 5 - (_in - last));
    strm.avail_out = (_out < end ? 257 + (end - _out) : 257 - (_out - end));
    state.hold = hold;
    state.bits = bits;
    return;
  };

  // (C) 1995-2013 Jean-loup Gailly and Mark Adler
  // (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
  //
  // This software is provided 'as-is', without any express or implied
  // warranty. In no event will the authors be held liable for any damages
  // arising from the use of this software.
  //
  // Permission is granted to anyone to use this software for any purpose,
  // including commercial applications, and to alter it and redistribute it
  // freely, subject to the following restrictions:
  //
  // 1. The origin of this software must not be misrepresented; you must not
  //   claim that you wrote the original software. If you use this software
  //   in a product, an acknowledgment in the product documentation would be
  //   appreciated but is not required.
  // 2. Altered source versions must be plainly marked as such, and must not be
  //   misrepresented as being the original software.
  // 3. This notice may not be removed or altered from any source distribution.

  const MAXBITS = 15;
  const ENOUGH_LENS$1 = 852;
  const ENOUGH_DISTS$1 = 592;
  //const ENOUGH = (ENOUGH_LENS+ENOUGH_DISTS);

  const CODES$1 = 0;
  const LENS$1 = 1;
  const DISTS$1 = 2;

  const lbase = new Uint16Array([ /* Length codes 257..285 base */
    3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,
    35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0
  ]);

  const lext = new Uint8Array([ /* Length codes 257..285 extra */
    16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18,
    19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 16, 72, 78
  ]);

  const dbase = new Uint16Array([ /* Distance codes 0..29 base */
    1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,
    257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145,
    8193, 12289, 16385, 24577, 0, 0
  ]);

  const dext = new Uint8Array([ /* Distance codes 0..29 extra */
    16, 16, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22,
    23, 23, 24, 24, 25, 25, 26, 26, 27, 27,
    28, 28, 29, 29, 64, 64
  ]);

  const inflate_table = (type, lens, lens_index, codes, table, table_index, work, opts) =>
  {
    const bits = opts.bits;
        //here = opts.here; /* table entry for duplication */

    let len = 0;               /* a code's length in bits */
    let sym = 0;               /* index of code symbols */
    let min = 0, max = 0;          /* minimum and maximum code lengths */
    let root = 0;              /* number of index bits for root table */
    let curr = 0;              /* number of index bits for current table */
    let drop = 0;              /* code bits to drop for sub-table */
    let left = 0;                   /* number of prefix codes available */
    let used = 0;              /* code entries in table used */
    let huff = 0;              /* Huffman code */
    let incr;              /* for incrementing code, index */
    let fill;              /* index for replicating entries */
    let low;               /* low bits for current root entry */
    let mask;              /* mask for low root bits */
    let next;             /* next available space in table */
    let base = null;     /* base value table to use */
  //  let shoextra;    /* extra bits table to use */
    let match;                  /* use base and extra for symbol >= match */
    const count = new Uint16Array(MAXBITS + 1); //[MAXBITS+1];    /* number of codes of each length */
    const offs = new Uint16Array(MAXBITS + 1); //[MAXBITS+1];     /* offsets in table for each length */
    let extra = null;

    let here_bits, here_op, here_val;

    /*
     Process a set of code lengths to create a canonical Huffman code.  The
     code lengths are lens[0..codes-1].  Each length corresponds to the
     symbols 0..codes-1.  The Huffman code is generated by first sorting the
     symbols by length from short to long, and retaining the symbol order
     for codes with equal lengths.  Then the code starts with all zero bits
     for the first code of the shortest length, and the codes are integer
     increments for the same length, and zeros are appended as the length
     increases.  For the deflate format, these bits are stored backwards
     from their more natural integer increment ordering, and so when the
     decoding tables are built in the large loop below, the integer codes
     are incremented backwards.

     This routine assumes, but does not check, that all of the entries in
     lens[] are in the range 0..MAXBITS.  The caller must assure this.
     1..MAXBITS is interpreted as that code length.  zero means that that
     symbol does not occur in this code.

     The codes are sorted by computing a count of codes for each length,
     creating from that a table of starting indices for each length in the
     sorted table, and then entering the symbols in order in the sorted
     table.  The sorted table is work[], with that space being provided by
     the caller.

     The length counts are used for other purposes as well, i.e. finding
     the minimum and maximum length codes, determining if there are any
     codes at all, checking for a valid set of lengths, and looking ahead
     at length counts to determine sub-table sizes when building the
     decoding tables.
     */

    /* accumulate lengths for codes (assumes lens[] all in 0..MAXBITS) */
    for (len = 0; len <= MAXBITS; len++) {
      count[len] = 0;
    }
    for (sym = 0; sym < codes; sym++) {
      count[lens[lens_index + sym]]++;
    }

    /* bound code lengths, force root to be within code lengths */
    root = bits;
    for (max = MAXBITS; max >= 1; max--) {
      if (count[max] !== 0) { break; }
    }
    if (root > max) {
      root = max;
    }
    if (max === 0) {                     /* no symbols to code at all */
      //table.op[opts.table_index] = 64;  //here.op = (var char)64;    /* invalid code marker */
      //table.bits[opts.table_index] = 1;   //here.bits = (var char)1;
      //table.val[opts.table_index++] = 0;   //here.val = (var short)0;
      table[table_index++] = (1 << 24) | (64 << 16) | 0;


      //table.op[opts.table_index] = 64;
      //table.bits[opts.table_index] = 1;
      //table.val[opts.table_index++] = 0;
      table[table_index++] = (1 << 24) | (64 << 16) | 0;

      opts.bits = 1;
      return 0;     /* no symbols, but wait for decoding to report error */
    }
    for (min = 1; min < max; min++) {
      if (count[min] !== 0) { break; }
    }
    if (root < min) {
      root = min;
    }

    /* check for an over-subscribed or incomplete set of lengths */
    left = 1;
    for (len = 1; len <= MAXBITS; len++) {
      left <<= 1;
      left -= count[len];
      if (left < 0) {
        return -1;
      }        /* over-subscribed */
    }
    if (left > 0 && (type === CODES$1 || max !== 1)) {
      return -1;                      /* incomplete set */
    }

    /* generate offsets into symbol table for each length for sorting */
    offs[1] = 0;
    for (len = 1; len < MAXBITS; len++) {
      offs[len + 1] = offs[len] + count[len];
    }

    /* sort symbols by length, by symbol order within each length */
    for (sym = 0; sym < codes; sym++) {
      if (lens[lens_index + sym] !== 0) {
        work[offs[lens[lens_index + sym]]++] = sym;
      }
    }

    /*
     Create and fill in decoding tables.  In this loop, the table being
     filled is at next and has curr index bits.  The code being used is huff
     with length len.  That code is converted to an index by dropping drop
     bits off of the bottom.  For codes where len is less than drop + curr,
     those top drop + curr - len bits are incremented through all values to
     fill the table with replicated entries.

     root is the number of index bits for the root table.  When len exceeds
     root, sub-tables are created pointed to by the root entry with an index
     of the low root bits of huff.  This is saved in low to check for when a
     new sub-table should be started.  drop is zero when the root table is
     being filled, and drop is root when sub-tables are being filled.

     When a new sub-table is needed, it is necessary to look ahead in the
     code lengths to determine what size sub-table is needed.  The length
     counts are used for this, and so count[] is decremented as codes are
     entered in the tables.

     used keeps track of how many table entries have been allocated from the
     provided *table space.  It is checked for LENS and DIST tables against
     the constants ENOUGH_LENS and ENOUGH_DISTS to guard against changes in
     the initial root table size constants.  See the comments in inftrees.h
     for more information.

     sym increments through all symbols, and the loop terminates when
     all codes of length max, i.e. all codes, have been processed.  This
     routine permits incomplete codes, so another loop after this one fills
     in the rest of the decoding tables with invalid code markers.
     */

    /* set up for code type */
    // poor man optimization - use if-else instead of switch,
    // to avoid deopts in old v8
    if (type === CODES$1) {
      base = extra = work;    /* dummy value--not used */
      match = 20;

    } else if (type === LENS$1) {
      base = lbase;
      extra = lext;
      match = 257;

    } else {                    /* DISTS */
      base = dbase;
      extra = dext;
      match = 0;
    }

    /* initialize opts for loop */
    huff = 0;                   /* starting code */
    sym = 0;                    /* starting code symbol */
    len = min;                  /* starting code length */
    next = table_index;              /* current table to fill in */
    curr = root;                /* current table index bits */
    drop = 0;                   /* current bits to drop from code for index */
    low = -1;                   /* trigger new sub-table when len > root */
    used = 1 << root;          /* use root table entries */
    mask = used - 1;            /* mask for comparing low */

    /* check available table space */
    if ((type === LENS$1 && used > ENOUGH_LENS$1) ||
      (type === DISTS$1 && used > ENOUGH_DISTS$1)) {
      return 1;
    }

    /* process all codes and make table entries */
    for (;;) {
      /* create table entry */
      here_bits = len - drop;
      if (work[sym] + 1 < match) {
        here_op = 0;
        here_val = work[sym];
      }
      else if (work[sym] >= match) {
        here_op = extra[work[sym] - match];
        here_val = base[work[sym] - match];
      }
      else {
        here_op = 32 + 64;         /* end of block */
        here_val = 0;
      }

      /* replicate for those indices with low len bits equal to huff */
      incr = 1 << (len - drop);
      fill = 1 << curr;
      min = fill;                 /* save offset to next table */
      do {
        fill -= incr;
        table[next + (huff >> drop) + fill] = (here_bits << 24) | (here_op << 16) | here_val |0;
      } while (fill !== 0);

      /* backwards increment the len-bit code huff */
      incr = 1 << (len - 1);
      while (huff & incr) {
        incr >>= 1;
      }
      if (incr !== 0) {
        huff &= incr - 1;
        huff += incr;
      } else {
        huff = 0;
      }

      /* go to next symbol, update count, len */
      sym++;
      if (--count[len] === 0) {
        if (len === max) { break; }
        len = lens[lens_index + work[sym]];
      }

      /* create new sub-table if needed */
      if (len > root && (huff & mask) !== low) {
        /* if first time, transition to sub-tables */
        if (drop === 0) {
          drop = root;
        }

        /* increment past last table */
        next += min;            /* here min is 1 << curr */

        /* determine length of next table */
        curr = len - drop;
        left = 1 << curr;
        while (curr + drop < max) {
          left -= count[curr + drop];
          if (left <= 0) { break; }
          curr++;
          left <<= 1;
        }

        /* check for enough space */
        used += 1 << curr;
        if ((type === LENS$1 && used > ENOUGH_LENS$1) ||
          (type === DISTS$1 && used > ENOUGH_DISTS$1)) {
          return 1;
        }

        /* point entry in root table to sub-table */
        low = huff & mask;
        /*table.op[low] = curr;
        table.bits[low] = root;
        table.val[low] = next - opts.table_index;*/
        table[low] = (root << 24) | (curr << 16) | (next - table_index) |0;
      }
    }

    /* fill in remaining table entry if code is incomplete (guaranteed to have
     at most one remaining entry, since if the code is incomplete, the
     maximum code length that was allowed to get this far is one bit) */
    if (huff !== 0) {
      //table.op[next + huff] = 64;            /* invalid code marker */
      //table.bits[next + huff] = len - drop;
      //table.val[next + huff] = 0;
      table[next + huff] = ((len - drop) << 24) | (64 << 16) |0;
    }

    /* set return parameters */
    //opts.table_index += used;
    opts.bits = root;
    return 0;
  };


  var inftrees = inflate_table;

  // (C) 1995-2013 Jean-loup Gailly and Mark Adler
  // (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
  //
  // This software is provided 'as-is', without any express or implied
  // warranty. In no event will the authors be held liable for any damages
  // arising from the use of this software.
  //
  // Permission is granted to anyone to use this software for any purpose,
  // including commercial applications, and to alter it and redistribute it
  // freely, subject to the following restrictions:
  //
  // 1. The origin of this software must not be misrepresented; you must not
  //   claim that you wrote the original software. If you use this software
  //   in a product, an acknowledgment in the product documentation would be
  //   appreciated but is not required.
  // 2. Altered source versions must be plainly marked as such, and must not be
  //   misrepresented as being the original software.
  // 3. This notice may not be removed or altered from any source distribution.






  const CODES = 0;
  const LENS = 1;
  const DISTS = 2;

  /* Public constants ==========================================================*/
  /* ===========================================================================*/

  const {
    Z_FINISH: Z_FINISH$1, Z_BLOCK, Z_TREES,
    Z_OK: Z_OK$1, Z_STREAM_END: Z_STREAM_END$1, Z_NEED_DICT: Z_NEED_DICT$1, Z_STREAM_ERROR: Z_STREAM_ERROR$1, Z_DATA_ERROR: Z_DATA_ERROR$1, Z_MEM_ERROR: Z_MEM_ERROR$1, Z_BUF_ERROR,
    Z_DEFLATED
  } = constants$2;


  /* STATES ====================================================================*/
  /* ===========================================================================*/


  const    HEAD = 16180;       /* i: waiting for magic header */
  const    FLAGS = 16181;      /* i: waiting for method and flags (gzip) */
  const    TIME = 16182;       /* i: waiting for modification time (gzip) */
  const    OS = 16183;         /* i: waiting for extra flags and operating system (gzip) */
  const    EXLEN = 16184;      /* i: waiting for extra length (gzip) */
  const    EXTRA = 16185;      /* i: waiting for extra bytes (gzip) */
  const    NAME = 16186;       /* i: waiting for end of file name (gzip) */
  const    COMMENT = 16187;    /* i: waiting for end of comment (gzip) */
  const    HCRC = 16188;       /* i: waiting for header crc (gzip) */
  const    DICTID = 16189;    /* i: waiting for dictionary check value */
  const    DICT = 16190;      /* waiting for inflateSetDictionary() call */
  const        TYPE = 16191;      /* i: waiting for type bits, including last-flag bit */
  const        TYPEDO = 16192;    /* i: same, but skip check to exit inflate on new block */
  const        STORED = 16193;    /* i: waiting for stored size (length and complement) */
  const        COPY_ = 16194;     /* i/o: same as COPY below, but only first time in */
  const        COPY = 16195;      /* i/o: waiting for input or output to copy stored block */
  const        TABLE = 16196;     /* i: waiting for dynamic block table lengths */
  const        LENLENS = 16197;   /* i: waiting for code length code lengths */
  const        CODELENS = 16198;  /* i: waiting for length/lit and distance code lengths */
  const            LEN_ = 16199;      /* i: same as LEN below, but only first time in */
  const            LEN = 16200;       /* i: waiting for length/lit/eob code */
  const            LENEXT = 16201;    /* i: waiting for length extra bits */
  const            DIST = 16202;      /* i: waiting for distance code */
  const            DISTEXT = 16203;   /* i: waiting for distance extra bits */
  const            MATCH = 16204;     /* o: waiting for output space to copy string */
  const            LIT = 16205;       /* o: waiting for output space to write literal */
  const    CHECK = 16206;     /* i: waiting for 32-bit check value */
  const    LENGTH = 16207;    /* i: waiting for 32-bit length (gzip) */
  const    DONE = 16208;      /* finished check, done -- remain here until reset */
  const    BAD = 16209;       /* got a data error -- remain here until reset */
  const    MEM = 16210;       /* got an inflate() memory error -- remain here until reset */
  const    SYNC = 16211;      /* looking for synchronization bytes to restart inflate() */

  /* ===========================================================================*/



  const ENOUGH_LENS = 852;
  const ENOUGH_DISTS = 592;
  //const ENOUGH =  (ENOUGH_LENS+ENOUGH_DISTS);

  const MAX_WBITS = 15;
  /* 32K LZ77 window */
  const DEF_WBITS = MAX_WBITS;


  const zswap32 = (q) => {

    return  (((q >>> 24) & 0xff) +
            ((q >>> 8) & 0xff00) +
            ((q & 0xff00) << 8) +
            ((q & 0xff) << 24));
  };


  function InflateState() {
    this.strm = null;           /* pointer back to this zlib stream */
    this.mode = 0;              /* current inflate mode */
    this.last = false;          /* true if processing last block */
    this.wrap = 0;              /* bit 0 true for zlib, bit 1 true for gzip,
                                   bit 2 true to validate check value */
    this.havedict = false;      /* true if dictionary provided */
    this.flags = 0;             /* gzip header method and flags (0 if zlib), or
                                   -1 if raw or no header yet */
    this.dmax = 0;              /* zlib header max distance (INFLATE_STRICT) */
    this.check = 0;             /* protected copy of check value */
    this.total = 0;             /* protected copy of output count */
    // TODO: may be {}
    this.head = null;           /* where to save gzip header information */

    /* sliding window */
    this.wbits = 0;             /* log base 2 of requested window size */
    this.wsize = 0;             /* window size or zero if not using window */
    this.whave = 0;             /* valid bytes in the window */
    this.wnext = 0;             /* window write index */
    this.window = null;         /* allocated sliding window, if needed */

    /* bit accumulator */
    this.hold = 0;              /* input bit accumulator */
    this.bits = 0;              /* number of bits in "in" */

    /* for string and stored block copying */
    this.length = 0;            /* literal or length of data to copy */
    this.offset = 0;            /* distance back to copy string from */

    /* for table and code decoding */
    this.extra = 0;             /* extra bits needed */

    /* fixed and dynamic code tables */
    this.lencode = null;          /* starting table for length/literal codes */
    this.distcode = null;         /* starting table for distance codes */
    this.lenbits = 0;           /* index bits for lencode */
    this.distbits = 0;          /* index bits for distcode */

    /* dynamic table building */
    this.ncode = 0;             /* number of code length code lengths */
    this.nlen = 0;              /* number of length code lengths */
    this.ndist = 0;             /* number of distance code lengths */
    this.have = 0;              /* number of code lengths in lens[] */
    this.next = null;              /* next available space in codes[] */

    this.lens = new Uint16Array(320); /* temporary storage for code lengths */
    this.work = new Uint16Array(288); /* work area for code table building */

    /*
     because we don't have pointers in js, we use lencode and distcode directly
     as buffers so we don't need codes
    */
    //this.codes = new Int32Array(ENOUGH);       /* space for code tables */
    this.lendyn = null;              /* dynamic table for length/literal codes (JS specific) */
    this.distdyn = null;             /* dynamic table for distance codes (JS specific) */
    this.sane = 0;                   /* if false, allow invalid distance too far */
    this.back = 0;                   /* bits back of last unprocessed length/lit */
    this.was = 0;                    /* initial length of match */
  }


  const inflateStateCheck = (strm) => {

    if (!strm) {
      return 1;
    }
    const state = strm.state;
    if (!state || state.strm !== strm ||
      state.mode < HEAD || state.mode > SYNC) {
      return 1;
    }
    return 0;
  };


  const inflateResetKeep = (strm) => {

    if (inflateStateCheck(strm)) { return Z_STREAM_ERROR$1; }
    const state = strm.state;
    strm.total_in = strm.total_out = state.total = 0;
    strm.msg = ''; /*Z_NULL*/
    if (state.wrap) {       /* to support ill-conceived Java test suite */
      strm.adler = state.wrap & 1;
    }
    state.mode = HEAD;
    state.last = 0;
    state.havedict = 0;
    state.flags = -1;
    state.dmax = 32768;
    state.head = null/*Z_NULL*/;
    state.hold = 0;
    state.bits = 0;
    //state.lencode = state.distcode = state.next = state.codes;
    state.lencode = state.lendyn = new Int32Array(ENOUGH_LENS);
    state.distcode = state.distdyn = new Int32Array(ENOUGH_DISTS);

    state.sane = 1;
    state.back = -1;
    //Tracev((stderr, "inflate: reset\n"));
    return Z_OK$1;
  };


  const inflateReset = (strm) => {

    if (inflateStateCheck(strm)) { return Z_STREAM_ERROR$1; }
    const state = strm.state;
    state.wsize = 0;
    state.whave = 0;
    state.wnext = 0;
    return inflateResetKeep(strm);

  };


  const inflateReset2 = (strm, windowBits) => {
    let wrap;

    /* get the state */
    if (inflateStateCheck(strm)) { return Z_STREAM_ERROR$1; }
    const state = strm.state;

    /* extract wrap request from windowBits parameter */
    if (windowBits < 0) {
      wrap = 0;
      windowBits = -windowBits;
    }
    else {
      wrap = (windowBits >> 4) + 5;
      if (windowBits < 48) {
        windowBits &= 15;
      }
    }

    /* set number of window bits, free window if different */
    if (windowBits && (windowBits < 8 || windowBits > 15)) {
      return Z_STREAM_ERROR$1;
    }
    if (state.window !== null && state.wbits !== windowBits) {
      state.window = null;
    }

    /* update state and reset the rest of it */
    state.wrap = wrap;
    state.wbits = windowBits;
    return inflateReset(strm);
  };


  const inflateInit2 = (strm, windowBits) => {

    if (!strm) { return Z_STREAM_ERROR$1; }
    //strm.msg = Z_NULL;                 /* in case we return an error */

    const state = new InflateState();

    //if (state === Z_NULL) return Z_MEM_ERROR;
    //Tracev((stderr, "inflate: allocated\n"));
    strm.state = state;
    state.strm = strm;
    state.window = null/*Z_NULL*/;
    state.mode = HEAD;     /* to pass state test in inflateReset2() */
    const ret = inflateReset2(strm, windowBits);
    if (ret !== Z_OK$1) {
      strm.state = null/*Z_NULL*/;
    }
    return ret;
  };


  const inflateInit = (strm) => {

    return inflateInit2(strm, DEF_WBITS);
  };


  /*
   Return state with length and distance decoding tables and index sizes set to
   fixed code decoding.  Normally this returns fixed tables from inffixed.h.
   If BUILDFIXED is defined, then instead this routine builds the tables the
   first time it's called, and returns those tables the first time and
   thereafter.  This reduces the size of the code by about 2K bytes, in
   exchange for a little execution time.  However, BUILDFIXED should not be
   used for threaded applications, since the rewriting of the tables and virgin
   may not be thread-safe.
   */
  let virgin = true;

  let lenfix, distfix; // We have no pointers in JS, so keep tables separate


  const fixedtables = (state) => {

    /* build fixed huffman tables if first call (may not be thread safe) */
    if (virgin) {
      lenfix = new Int32Array(512);
      distfix = new Int32Array(32);

      /* literal/length table */
      let sym = 0;
      while (sym < 144) { state.lens[sym++] = 8; }
      while (sym < 256) { state.lens[sym++] = 9; }
      while (sym < 280) { state.lens[sym++] = 7; }
      while (sym < 288) { state.lens[sym++] = 8; }

      inftrees(LENS,  state.lens, 0, 288, lenfix,   0, state.work, { bits: 9 });

      /* distance table */
      sym = 0;
      while (sym < 32) { state.lens[sym++] = 5; }

      inftrees(DISTS, state.lens, 0, 32,   distfix, 0, state.work, { bits: 5 });

      /* do this just once */
      virgin = false;
    }

    state.lencode = lenfix;
    state.lenbits = 9;
    state.distcode = distfix;
    state.distbits = 5;
  };


  /*
   Update the window with the last wsize (normally 32K) bytes written before
   returning.  If window does not exist yet, create it.  This is only called
   when a window is already in use, or when output has been written during this
   inflate call, but the end of the deflate stream has not been reached yet.
   It is also called to create a window for dictionary data when a dictionary
   is loaded.

   Providing output buffers larger than 32K to inflate() should provide a speed
   advantage, since only the last 32K of output is copied to the sliding window
   upon return from inflate(), and since all distances after the first 32K of
   output will fall in the output data, making match copies simpler and faster.
   The advantage may be dependent on the size of the processor's data caches.
   */
  const updatewindow = (strm, src, end, copy) => {

    let dist;
    const state = strm.state;

    /* if it hasn't been done already, allocate space for the window */
    if (state.window === null) {
      state.wsize = 1 << state.wbits;
      state.wnext = 0;
      state.whave = 0;

      state.window = new Uint8Array(state.wsize);
    }

    /* copy state->wsize or less output bytes into the circular window */
    if (copy >= state.wsize) {
      state.window.set(src.subarray(end - state.wsize, end), 0);
      state.wnext = 0;
      state.whave = state.wsize;
    }
    else {
      dist = state.wsize - state.wnext;
      if (dist > copy) {
        dist = copy;
      }
      //zmemcpy(state->window + state->wnext, end - copy, dist);
      state.window.set(src.subarray(end - copy, end - copy + dist), state.wnext);
      copy -= dist;
      if (copy) {
        //zmemcpy(state->window, end - copy, copy);
        state.window.set(src.subarray(end - copy, end), 0);
        state.wnext = copy;
        state.whave = state.wsize;
      }
      else {
        state.wnext += dist;
        if (state.wnext === state.wsize) { state.wnext = 0; }
        if (state.whave < state.wsize) { state.whave += dist; }
      }
    }
    return 0;
  };


  const inflate$2 = (strm, flush) => {

    let state;
    let input, output;          // input/output buffers
    let next;                   /* next input INDEX */
    let put;                    /* next output INDEX */
    let have, left;             /* available input and output */
    let hold;                   /* bit buffer */
    let bits;                   /* bits in bit buffer */
    let _in, _out;              /* save starting available input and output */
    let copy;                   /* number of stored or match bytes to copy */
    let from;                   /* where to copy match bytes from */
    let from_source;
    let here = 0;               /* current decoding table entry */
    let here_bits, here_op, here_val; // paked "here" denormalized (JS specific)
    //let last;                   /* parent table entry */
    let last_bits, last_op, last_val; // paked "last" denormalized (JS specific)
    let len;                    /* length to copy for repeats, bits to drop */
    let ret;                    /* return code */
    const hbuf = new Uint8Array(4);    /* buffer for gzip header crc calculation */
    let opts;

    let n; // temporary variable for NEED_BITS

    const order = /* permutation of code lengths */
      new Uint8Array([ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ]);


    if (inflateStateCheck(strm) || !strm.output ||
        (!strm.input && strm.avail_in !== 0)) {
      return Z_STREAM_ERROR$1;
    }

    state = strm.state;
    if (state.mode === TYPE) { state.mode = TYPEDO; }    /* skip check */


    //--- LOAD() ---
    put = strm.next_out;
    output = strm.output;
    left = strm.avail_out;
    next = strm.next_in;
    input = strm.input;
    have = strm.avail_in;
    hold = state.hold;
    bits = state.bits;
    //---

    _in = have;
    _out = left;
    ret = Z_OK$1;

    inf_leave: // goto emulation
    for (;;) {
      switch (state.mode) {
        case HEAD:
          if (state.wrap === 0) {
            state.mode = TYPEDO;
            break;
          }
          //=== NEEDBITS(16);
          while (bits < 16) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          if ((state.wrap & 2) && hold === 0x8b1f) {  /* gzip header */
            if (state.wbits === 0) {
              state.wbits = 15;
            }
            state.check = 0/*crc32(0L, Z_NULL, 0)*/;
            //=== CRC2(state.check, hold);
            hbuf[0] = hold & 0xff;
            hbuf[1] = (hold >>> 8) & 0xff;
            state.check = crc32_1(state.check, hbuf, 2, 0);
            //===//

            //=== INITBITS();
            hold = 0;
            bits = 0;
            //===//
            state.mode = FLAGS;
            break;
          }
          if (state.head) {
            state.head.done = false;
          }
          if (!(state.wrap & 1) ||   /* check if zlib header allowed */
            (((hold & 0xff)/*BITS(8)*/ << 8) + (hold >> 8)) % 31) {
            strm.msg = 'incorrect header check';
            state.mode = BAD;
            break;
          }
          if ((hold & 0x0f)/*BITS(4)*/ !== Z_DEFLATED) {
            strm.msg = 'unknown compression method';
            state.mode = BAD;
            break;
          }
          //--- DROPBITS(4) ---//
          hold >>>= 4;
          bits -= 4;
          //---//
          len = (hold & 0x0f)/*BITS(4)*/ + 8;
          if (state.wbits === 0) {
            state.wbits = len;
          }
          if (len > 15 || len > state.wbits) {
            strm.msg = 'invalid window size';
            state.mode = BAD;
            break;
          }

          // !!! pako patch. Force use `options.windowBits` if passed.
          // Required to always use max window size by default.
          state.dmax = 1 << state.wbits;
          //state.dmax = 1 << len;

          state.flags = 0;               /* indicate zlib header */
          //Tracev((stderr, "inflate:   zlib header ok\n"));
          strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/;
          state.mode = hold & 0x200 ? DICTID : TYPE;
          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
          break;
        case FLAGS:
          //=== NEEDBITS(16); */
          while (bits < 16) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          state.flags = hold;
          if ((state.flags & 0xff) !== Z_DEFLATED) {
            strm.msg = 'unknown compression method';
            state.mode = BAD;
            break;
          }
          if (state.flags & 0xe000) {
            strm.msg = 'unknown header flags set';
            state.mode = BAD;
            break;
          }
          if (state.head) {
            state.head.text = ((hold >> 8) & 1);
          }
          if ((state.flags & 0x0200) && (state.wrap & 4)) {
            //=== CRC2(state.check, hold);
            hbuf[0] = hold & 0xff;
            hbuf[1] = (hold >>> 8) & 0xff;
            state.check = crc32_1(state.check, hbuf, 2, 0);
            //===//
          }
          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
          state.mode = TIME;
          /* falls through */
        case TIME:
          //=== NEEDBITS(32); */
          while (bits < 32) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          if (state.head) {
            state.head.time = hold;
          }
          if ((state.flags & 0x0200) && (state.wrap & 4)) {
            //=== CRC4(state.check, hold)
            hbuf[0] = hold & 0xff;
            hbuf[1] = (hold >>> 8) & 0xff;
            hbuf[2] = (hold >>> 16) & 0xff;
            hbuf[3] = (hold >>> 24) & 0xff;
            state.check = crc32_1(state.check, hbuf, 4, 0);
            //===
          }
          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
          state.mode = OS;
          /* falls through */
        case OS:
          //=== NEEDBITS(16); */
          while (bits < 16) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          if (state.head) {
            state.head.xflags = (hold & 0xff);
            state.head.os = (hold >> 8);
          }
          if ((state.flags & 0x0200) && (state.wrap & 4)) {
            //=== CRC2(state.check, hold);
            hbuf[0] = hold & 0xff;
            hbuf[1] = (hold >>> 8) & 0xff;
            state.check = crc32_1(state.check, hbuf, 2, 0);
            //===//
          }
          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
          state.mode = EXLEN;
          /* falls through */
        case EXLEN:
          if (state.flags & 0x0400) {
            //=== NEEDBITS(16); */
            while (bits < 16) {
              if (have === 0) { break inf_leave; }
              have--;
              hold += input[next++] << bits;
              bits += 8;
            }
            //===//
            state.length = hold;
            if (state.head) {
              state.head.extra_len = hold;
            }
            if ((state.flags & 0x0200) && (state.wrap & 4)) {
              //=== CRC2(state.check, hold);
              hbuf[0] = hold & 0xff;
              hbuf[1] = (hold >>> 8) & 0xff;
              state.check = crc32_1(state.check, hbuf, 2, 0);
              //===//
            }
            //=== INITBITS();
            hold = 0;
            bits = 0;
            //===//
          }
          else if (state.head) {
            state.head.extra = null/*Z_NULL*/;
          }
          state.mode = EXTRA;
          /* falls through */
        case EXTRA:
          if (state.flags & 0x0400) {
            copy = state.length;
            if (copy > have) { copy = have; }
            if (copy) {
              if (state.head) {
                len = state.head.extra_len - state.length;
                if (!state.head.extra) {
                  // Use untyped array for more convenient processing later
                  state.head.extra = new Uint8Array(state.head.extra_len);
                }
                state.head.extra.set(
                  input.subarray(
                    next,
                    // extra field is limited to 65536 bytes
                    // - no need for additional size check
                    next + copy
                  ),
                  /*len + copy > state.head.extra_max - len ? state.head.extra_max : copy,*/
                  len
                );
                //zmemcpy(state.head.extra + len, next,
                //        len + copy > state.head.extra_max ?
                //        state.head.extra_max - len : copy);
              }
              if ((state.flags & 0x0200) && (state.wrap & 4)) {
                state.check = crc32_1(state.check, input, copy, next);
              }
              have -= copy;
              next += copy;
              state.length -= copy;
            }
            if (state.length) { break inf_leave; }
          }
          state.length = 0;
          state.mode = NAME;
          /* falls through */
        case NAME:
          if (state.flags & 0x0800) {
            if (have === 0) { break inf_leave; }
            copy = 0;
            do {
              // TODO: 2 or 1 bytes?
              len = input[next + copy++];
              /* use constant limit because in js we should not preallocate memory */
              if (state.head && len &&
                  (state.length < 65536 /*state.head.name_max*/)) {
                state.head.name += String.fromCharCode(len);
              }
            } while (len && copy < have);

            if ((state.flags & 0x0200) && (state.wrap & 4)) {
              state.check = crc32_1(state.check, input, copy, next);
            }
            have -= copy;
            next += copy;
            if (len) { break inf_leave; }
          }
          else if (state.head) {
            state.head.name = null;
          }
          state.length = 0;
          state.mode = COMMENT;
          /* falls through */
        case COMMENT:
          if (state.flags & 0x1000) {
            if (have === 0) { break inf_leave; }
            copy = 0;
            do {
              len = input[next + copy++];
              /* use constant limit because in js we should not preallocate memory */
              if (state.head && len &&
                  (state.length < 65536 /*state.head.comm_max*/)) {
                state.head.comment += String.fromCharCode(len);
              }
            } while (len && copy < have);
            if ((state.flags & 0x0200) && (state.wrap & 4)) {
              state.check = crc32_1(state.check, input, copy, next);
            }
            have -= copy;
            next += copy;
            if (len) { break inf_leave; }
          }
          else if (state.head) {
            state.head.comment = null;
          }
          state.mode = HCRC;
          /* falls through */
        case HCRC:
          if (state.flags & 0x0200) {
            //=== NEEDBITS(16); */
            while (bits < 16) {
              if (have === 0) { break inf_leave; }
              have--;
              hold += input[next++] << bits;
              bits += 8;
            }
            //===//
            if ((state.wrap & 4) && hold !== (state.check & 0xffff)) {
              strm.msg = 'header crc mismatch';
              state.mode = BAD;
              break;
            }
            //=== INITBITS();
            hold = 0;
            bits = 0;
            //===//
          }
          if (state.head) {
            state.head.hcrc = ((state.flags >> 9) & 1);
            state.head.done = true;
          }
          strm.adler = state.check = 0;
          state.mode = TYPE;
          break;
        case DICTID:
          //=== NEEDBITS(32); */
          while (bits < 32) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          strm.adler = state.check = zswap32(hold);
          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
          state.mode = DICT;
          /* falls through */
        case DICT:
          if (state.havedict === 0) {
            //--- RESTORE() ---
            strm.next_out = put;
            strm.avail_out = left;
            strm.next_in = next;
            strm.avail_in = have;
            state.hold = hold;
            state.bits = bits;
            //---
            return Z_NEED_DICT$1;
          }
          strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/;
          state.mode = TYPE;
          /* falls through */
        case TYPE:
          if (flush === Z_BLOCK || flush === Z_TREES) { break inf_leave; }
          /* falls through */
        case TYPEDO:
          if (state.last) {
            //--- BYTEBITS() ---//
            hold >>>= bits & 7;
            bits -= bits & 7;
            //---//
            state.mode = CHECK;
            break;
          }
          //=== NEEDBITS(3); */
          while (bits < 3) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          state.last = (hold & 0x01)/*BITS(1)*/;
          //--- DROPBITS(1) ---//
          hold >>>= 1;
          bits -= 1;
          //---//

          switch ((hold & 0x03)/*BITS(2)*/) {
            case 0:                             /* stored block */
              //Tracev((stderr, "inflate:     stored block%s\n",
              //        state.last ? " (last)" : ""));
              state.mode = STORED;
              break;
            case 1:                             /* fixed block */
              fixedtables(state);
              //Tracev((stderr, "inflate:     fixed codes block%s\n",
              //        state.last ? " (last)" : ""));
              state.mode = LEN_;             /* decode codes */
              if (flush === Z_TREES) {
                //--- DROPBITS(2) ---//
                hold >>>= 2;
                bits -= 2;
                //---//
                break inf_leave;
              }
              break;
            case 2:                             /* dynamic block */
              //Tracev((stderr, "inflate:     dynamic codes block%s\n",
              //        state.last ? " (last)" : ""));
              state.mode = TABLE;
              break;
            case 3:
              strm.msg = 'invalid block type';
              state.mode = BAD;
          }
          //--- DROPBITS(2) ---//
          hold >>>= 2;
          bits -= 2;
          //---//
          break;
        case STORED:
          //--- BYTEBITS() ---// /* go to byte boundary */
          hold >>>= bits & 7;
          bits -= bits & 7;
          //---//
          //=== NEEDBITS(32); */
          while (bits < 32) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          if ((hold & 0xffff) !== ((hold >>> 16) ^ 0xffff)) {
            strm.msg = 'invalid stored block lengths';
            state.mode = BAD;
            break;
          }
          state.length = hold & 0xffff;
          //Tracev((stderr, "inflate:       stored length %u\n",
          //        state.length));
          //=== INITBITS();
          hold = 0;
          bits = 0;
          //===//
          state.mode = COPY_;
          if (flush === Z_TREES) { break inf_leave; }
          /* falls through */
        case COPY_:
          state.mode = COPY;
          /* falls through */
        case COPY:
          copy = state.length;
          if (copy) {
            if (copy > have) { copy = have; }
            if (copy > left) { copy = left; }
            if (copy === 0) { break inf_leave; }
            //--- zmemcpy(put, next, copy); ---
            output.set(input.subarray(next, next + copy), put);
            //---//
            have -= copy;
            next += copy;
            left -= copy;
            put += copy;
            state.length -= copy;
            break;
          }
          //Tracev((stderr, "inflate:       stored end\n"));
          state.mode = TYPE;
          break;
        case TABLE:
          //=== NEEDBITS(14); */
          while (bits < 14) {
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
          }
          //===//
          state.nlen = (hold & 0x1f)/*BITS(5)*/ + 257;
          //--- DROPBITS(5) ---//
          hold >>>= 5;
          bits -= 5;
          //---//
          state.ndist = (hold & 0x1f)/*BITS(5)*/ + 1;
          //--- DROPBITS(5) ---//
          hold >>>= 5;
          bits -= 5;
          //---//
          state.ncode = (hold & 0x0f)/*BITS(4)*/ + 4;
          //--- DROPBITS(4) ---//
          hold >>>= 4;
          bits -= 4;
          //---//
  //#ifndef PKZIP_BUG_WORKAROUND
          if (state.nlen > 286 || state.ndist > 30) {
            strm.msg = 'too many length or distance symbols';
            state.mode = BAD;
            break;
          }
  //#endif
          //Tracev((stderr, "inflate:       table sizes ok\n"));
          state.have = 0;
          state.mode = LENLENS;
          /* falls through */
        case LENLENS:
          while (state.have < state.ncode) {
            //=== NEEDBITS(3);
            while (bits < 3) {
              if (have === 0) { break inf_leave; }
              have--;
              hold += input[next++] << bits;
              bits += 8;
            }
            //===//
            state.lens[order[state.have++]] = (hold & 0x07);//BITS(3);
            //--- DROPBITS(3) ---//
            hold >>>= 3;
            bits -= 3;
            //---//
          }
          while (state.have < 19) {
            state.lens[order[state.have++]] = 0;
          }
          // We have separate tables & no pointers. 2 commented lines below not needed.
          //state.next = state.codes;
          //state.lencode = state.next;
          // Switch to use dynamic table
          state.lencode = state.lendyn;
          state.lenbits = 7;

          opts = { bits: state.lenbits };
          ret = inftrees(CODES, state.lens, 0, 19, state.lencode, 0, state.work, opts);
          state.lenbits = opts.bits;

          if (ret) {
            strm.msg = 'invalid code lengths set';
            state.mode = BAD;
            break;
          }
          //Tracev((stderr, "inflate:       code lengths ok\n"));
          state.have = 0;
          state.mode = CODELENS;
          /* falls through */
        case CODELENS:
          while (state.have < state.nlen + state.ndist) {
            for (;;) {
              here = state.lencode[hold & ((1 << state.lenbits) - 1)];/*BITS(state.lenbits)*/
              here_bits = here >>> 24;
              here_op = (here >>> 16) & 0xff;
              here_val = here & 0xffff;

              if ((here_bits) <= bits) { break; }
              //--- PULLBYTE() ---//
              if (have === 0) { break inf_leave; }
              have--;
              hold += input[next++] << bits;
              bits += 8;
              //---//
            }
            if (here_val < 16) {
              //--- DROPBITS(here.bits) ---//
              hold >>>= here_bits;
              bits -= here_bits;
              //---//
              state.lens[state.have++] = here_val;
            }
            else {
              if (here_val === 16) {
                //=== NEEDBITS(here.bits + 2);
                n = here_bits + 2;
                while (bits < n) {
                  if (have === 0) { break inf_leave; }
                  have--;
                  hold += input[next++] << bits;
                  bits += 8;
                }
                //===//
                //--- DROPBITS(here.bits) ---//
                hold >>>= here_bits;
                bits -= here_bits;
                //---//
                if (state.have === 0) {
                  strm.msg = 'invalid bit length repeat';
                  state.mode = BAD;
                  break;
                }
                len = state.lens[state.have - 1];
                copy = 3 + (hold & 0x03);//BITS(2);
                //--- DROPBITS(2) ---//
                hold >>>= 2;
                bits -= 2;
                //---//
              }
              else if (here_val === 17) {
                //=== NEEDBITS(here.bits + 3);
                n = here_bits + 3;
                while (bits < n) {
                  if (have === 0) { break inf_leave; }
                  have--;
                  hold += input[next++] << bits;
                  bits += 8;
                }
                //===//
                //--- DROPBITS(here.bits) ---//
                hold >>>= here_bits;
                bits -= here_bits;
                //---//
                len = 0;
                copy = 3 + (hold & 0x07);//BITS(3);
                //--- DROPBITS(3) ---//
                hold >>>= 3;
                bits -= 3;
                //---//
              }
              else {
                //=== NEEDBITS(here.bits + 7);
                n = here_bits + 7;
                while (bits < n) {
                  if (have === 0) { break inf_leave; }
                  have--;
                  hold += input[next++] << bits;
                  bits += 8;
                }
                //===//
                //--- DROPBITS(here.bits) ---//
                hold >>>= here_bits;
                bits -= here_bits;
                //---//
                len = 0;
                copy = 11 + (hold & 0x7f);//BITS(7);
                //--- DROPBITS(7) ---//
                hold >>>= 7;
                bits -= 7;
                //---//
              }
              if (state.have + copy > state.nlen + state.ndist) {
                strm.msg = 'invalid bit length repeat';
                state.mode = BAD;
                break;
              }
              while (copy--) {
                state.lens[state.have++] = len;
              }
            }
          }

          /* handle error breaks in while */
          if (state.mode === BAD) { break; }

          /* check for end-of-block code (better have one) */
          if (state.lens[256] === 0) {
            strm.msg = 'invalid code -- missing end-of-block';
            state.mode = BAD;
            break;
          }

          /* build code tables -- note: do not change the lenbits or distbits
             values here (9 and 6) without reading the comments in inftrees.h
             concerning the ENOUGH constants, which depend on those values */
          state.lenbits = 9;

          opts = { bits: state.lenbits };
          ret = inftrees(LENS, state.lens, 0, state.nlen, state.lencode, 0, state.work, opts);
          // We have separate tables & no pointers. 2 commented lines below not needed.
          // state.next_index = opts.table_index;
          state.lenbits = opts.bits;
          // state.lencode = state.next;

          if (ret) {
            strm.msg = 'invalid literal/lengths set';
            state.mode = BAD;
            break;
          }

          state.distbits = 6;
          //state.distcode.copy(state.codes);
          // Switch to use dynamic table
          state.distcode = state.distdyn;
          opts = { bits: state.distbits };
          ret = inftrees(DISTS, state.lens, state.nlen, state.ndist, state.distcode, 0, state.work, opts);
          // We have separate tables & no pointers. 2 commented lines below not needed.
          // state.next_index = opts.table_index;
          state.distbits = opts.bits;
          // state.distcode = state.next;

          if (ret) {
            strm.msg = 'invalid distances set';
            state.mode = BAD;
            break;
          }
          //Tracev((stderr, 'inflate:       codes ok\n'));
          state.mode = LEN_;
          if (flush === Z_TREES) { break inf_leave; }
          /* falls through */
        case LEN_:
          state.mode = LEN;
          /* falls through */
        case LEN:
          if (have >= 6 && left >= 258) {
            //--- RESTORE() ---
            strm.next_out = put;
            strm.avail_out = left;
            strm.next_in = next;
            strm.avail_in = have;
            state.hold = hold;
            state.bits = bits;
            //---
            inffast(strm, _out);
            //--- LOAD() ---
            put = strm.next_out;
            output = strm.output;
            left = strm.avail_out;
            next = strm.next_in;
            input = strm.input;
            have = strm.avail_in;
            hold = state.hold;
            bits = state.bits;
            //---

            if (state.mode === TYPE) {
              state.back = -1;
            }
            break;
          }
          state.back = 0;
          for (;;) {
            here = state.lencode[hold & ((1 << state.lenbits) - 1)];  /*BITS(state.lenbits)*/
            here_bits = here >>> 24;
            here_op = (here >>> 16) & 0xff;
            here_val = here & 0xffff;

            if (here_bits <= bits) { break; }
            //--- PULLBYTE() ---//
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
            //---//
          }
          if (here_op && (here_op & 0xf0) === 0) {
            last_bits = here_bits;
            last_op = here_op;
            last_val = here_val;
            for (;;) {
              here = state.lencode[last_val +
                      ((hold & ((1 << (last_bits + last_op)) - 1))/*BITS(last.bits + last.op)*/ >> last_bits)];
              here_bits = here >>> 24;
              here_op = (here >>> 16) & 0xff;
              here_val = here & 0xffff;

              if ((last_bits + here_bits) <= bits) { break; }
              //--- PULLBYTE() ---//
              if (have === 0) { break inf_leave; }
              have--;
              hold += input[next++] << bits;
              bits += 8;
              //---//
            }
            //--- DROPBITS(last.bits) ---//
            hold >>>= last_bits;
            bits -= last_bits;
            //---//
            state.back += last_bits;
          }
          //--- DROPBITS(here.bits) ---//
          hold >>>= here_bits;
          bits -= here_bits;
          //---//
          state.back += here_bits;
          state.length = here_val;
          if (here_op === 0) {
            //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ?
            //        "inflate:         literal '%c'\n" :
            //        "inflate:         literal 0x%02x\n", here.val));
            state.mode = LIT;
            break;
          }
          if (here_op & 32) {
            //Tracevv((stderr, "inflate:         end of block\n"));
            state.back = -1;
            state.mode = TYPE;
            break;
          }
          if (here_op & 64) {
            strm.msg = 'invalid literal/length code';
            state.mode = BAD;
            break;
          }
          state.extra = here_op & 15;
          state.mode = LENEXT;
          /* falls through */
        case LENEXT:
          if (state.extra) {
            //=== NEEDBITS(state.extra);
            n = state.extra;
            while (bits < n) {
              if (have === 0) { break inf_leave; }
              have--;
              hold += input[next++] << bits;
              bits += 8;
            }
            //===//
            state.length += hold & ((1 << state.extra) - 1)/*BITS(state.extra)*/;
            //--- DROPBITS(state.extra) ---//
            hold >>>= state.extra;
            bits -= state.extra;
            //---//
            state.back += state.extra;
          }
          //Tracevv((stderr, "inflate:         length %u\n", state.length));
          state.was = state.length;
          state.mode = DIST;
          /* falls through */
        case DIST:
          for (;;) {
            here = state.distcode[hold & ((1 << state.distbits) - 1)];/*BITS(state.distbits)*/
            here_bits = here >>> 24;
            here_op = (here >>> 16) & 0xff;
            here_val = here & 0xffff;

            if ((here_bits) <= bits) { break; }
            //--- PULLBYTE() ---//
            if (have === 0) { break inf_leave; }
            have--;
            hold += input[next++] << bits;
            bits += 8;
            //---//
          }
          if ((here_op & 0xf0) === 0) {
            last_bits = here_bits;
            last_op = here_op;
            last_val = here_val;
            for (;;) {
              here = state.distcode[last_val +
                      ((hold & ((1 << (last_bits + last_op)) - 1))/*BITS(last.bits + last.op)*/ >> last_bits)];
              here_bits = here >>> 24;
              here_op = (here >>> 16) & 0xff;
              here_val = here & 0xffff;

              if ((last_bits + here_bits) <= bits) { break; }
              //--- PULLBYTE() ---//
              if (have === 0) { break inf_leave; }
              have--;
              hold += input[next++] << bits;
              bits += 8;
              //---//
            }
            //--- DROPBITS(last.bits) ---//
            hold >>>= last_bits;
            bits -= last_bits;
            //---//
            state.back += last_bits;
          }
          //--- DROPBITS(here.bits) ---//
          hold >>>= here_bits;
          bits -= here_bits;
          //---//
          state.back += here_bits;
          if (here_op & 64) {
            strm.msg = 'invalid distance code';
            state.mode = BAD;
            break;
          }
          state.offset = here_val;
          state.extra = (here_op) & 15;
          state.mode = DISTEXT;
          /* falls through */
        case DISTEXT:
          if (state.extra) {
            //=== NEEDBITS(state.extra);
            n = state.extra;
            while (bits < n) {
              if (have === 0) { break inf_leave; }
              have--;
              hold += input[next++] << bits;
              bits += 8;
            }
            //===//
            state.offset += hold & ((1 << state.extra) - 1)/*BITS(state.extra)*/;
            //--- DROPBITS(state.extra) ---//
            hold >>>= state.extra;
            bits -= state.extra;
            //---//
            state.back += state.extra;
          }
  //#ifdef INFLATE_STRICT
          if (state.offset > state.dmax) {
            strm.msg = 'invalid distance too far back';
            state.mode = BAD;
            break;
          }
  //#endif
          //Tracevv((stderr, "inflate:         distance %u\n", state.offset));
          state.mode = MATCH;
          /* falls through */
        case MATCH:
          if (left === 0) { break inf_leave; }
          copy = _out - left;
          if (state.offset > copy) {         /* copy from window */
            copy = state.offset - copy;
            if (copy > state.whave) {
              if (state.sane) {
                strm.msg = 'invalid distance too far back';
                state.mode = BAD;
                break;
              }
  // (!) This block is disabled in zlib defaults,
  // don't enable it for binary compatibility
  //#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR
  //          Trace((stderr, "inflate.c too far\n"));
  //          copy -= state.whave;
  //          if (copy > state.length) { copy = state.length; }
  //          if (copy > left) { copy = left; }
  //          left -= copy;
  //          state.length -= copy;
  //          do {
  //            output[put++] = 0;
  //          } while (--copy);
  //          if (state.length === 0) { state.mode = LEN; }
  //          break;
  //#endif
            }
            if (copy > state.wnext) {
              copy -= state.wnext;
              from = state.wsize - copy;
            }
            else {
              from = state.wnext - copy;
            }
            if (copy > state.length) { copy = state.length; }
            from_source = state.window;
          }
          else {                              /* copy from output */
            from_source = output;
            from = put - state.offset;
            copy = state.length;
          }
          if (copy > left) { copy = left; }
          left -= copy;
          state.length -= copy;
          do {
            output[put++] = from_source[from++];
          } while (--copy);
          if (state.length === 0) { state.mode = LEN; }
          break;
        case LIT:
          if (left === 0) { break inf_leave; }
          output[put++] = state.length;
          left--;
          state.mode = LEN;
          break;
        case CHECK:
          if (state.wrap) {
            //=== NEEDBITS(32);
            while (bits < 32) {
              if (have === 0) { break inf_leave; }
              have--;
              // Use '|' instead of '+' to make sure that result is signed
              hold |= input[next++] << bits;
              bits += 8;
            }
            //===//
            _out -= left;
            strm.total_out += _out;
            state.total += _out;
            if ((state.wrap & 4) && _out) {
              strm.adler = state.check =
                  /*UPDATE_CHECK(state.check, put - _out, _out);*/
                  (state.flags ? crc32_1(state.check, output, _out, put - _out) : adler32_1(state.check, output, _out, put - _out));

            }
            _out = left;
            // NB: crc32 stored as signed 32-bit int, zswap32 returns signed too
            if ((state.wrap & 4) && (state.flags ? hold : zswap32(hold)) !== state.check) {
              strm.msg = 'incorrect data check';
              state.mode = BAD;
              break;
            }
            //=== INITBITS();
            hold = 0;
            bits = 0;
            //===//
            //Tracev((stderr, "inflate:   check matches trailer\n"));
          }
          state.mode = LENGTH;
          /* falls through */
        case LENGTH:
          if (state.wrap && state.flags) {
            //=== NEEDBITS(32);
            while (bits < 32) {
              if (have === 0) { break inf_leave; }
              have--;
              hold += input[next++] << bits;
              bits += 8;
            }
            //===//
            if ((state.wrap & 4) && hold !== (state.total & 0xffffffff)) {
              strm.msg = 'incorrect length check';
              state.mode = BAD;
              break;
            }
            //=== INITBITS();
            hold = 0;
            bits = 0;
            //===//
            //Tracev((stderr, "inflate:   length matches trailer\n"));
          }
          state.mode = DONE;
          /* falls through */
        case DONE:
          ret = Z_STREAM_END$1;
          break inf_leave;
        case BAD:
          ret = Z_DATA_ERROR$1;
          break inf_leave;
        case MEM:
          return Z_MEM_ERROR$1;
        case SYNC:
          /* falls through */
        default:
          return Z_STREAM_ERROR$1;
      }
    }

    // inf_leave <- here is real place for "goto inf_leave", emulated via "break inf_leave"

    /*
       Return from inflate(), updating the total counts and the check value.
       If there was no progress during the inflate() call, return a buffer
       error.  Call updatewindow() to create and/or update the window state.
       Note: a memory error from inflate() is non-recoverable.
     */

    //--- RESTORE() ---
    strm.next_out = put;
    strm.avail_out = left;
    strm.next_in = next;
    strm.avail_in = have;
    state.hold = hold;
    state.bits = bits;
    //---

    if (state.wsize || (_out !== strm.avail_out && state.mode < BAD &&
                        (state.mode < CHECK || flush !== Z_FINISH$1))) {
      if (updatewindow(strm, strm.output, strm.next_out, _out - strm.avail_out)) ;
    }
    _in -= strm.avail_in;
    _out -= strm.avail_out;
    strm.total_in += _in;
    strm.total_out += _out;
    state.total += _out;
    if ((state.wrap & 4) && _out) {
      strm.adler = state.check = /*UPDATE_CHECK(state.check, strm.next_out - _out, _out);*/
        (state.flags ? crc32_1(state.check, output, _out, strm.next_out - _out) : adler32_1(state.check, output, _out, strm.next_out - _out));
    }
    strm.data_type = state.bits + (state.last ? 64 : 0) +
                      (state.mode === TYPE ? 128 : 0) +
                      (state.mode === LEN_ || state.mode === COPY_ ? 256 : 0);
    if (((_in === 0 && _out === 0) || flush === Z_FINISH$1) && ret === Z_OK$1) {
      ret = Z_BUF_ERROR;
    }
    return ret;
  };


  const inflateEnd = (strm) => {

    if (inflateStateCheck(strm)) {
      return Z_STREAM_ERROR$1;
    }

    let state = strm.state;
    if (state.window) {
      state.window = null;
    }
    strm.state = null;
    return Z_OK$1;
  };


  const inflateGetHeader = (strm, head) => {

    /* check state */
    if (inflateStateCheck(strm)) { return Z_STREAM_ERROR$1; }
    const state = strm.state;
    if ((state.wrap & 2) === 0) { return Z_STREAM_ERROR$1; }

    /* save header structure */
    state.head = head;
    head.done = false;
    return Z_OK$1;
  };


  const inflateSetDictionary = (strm, dictionary) => {
    const dictLength = dictionary.length;

    let state;
    let dictid;
    let ret;

    /* check state */
    if (inflateStateCheck(strm)) { return Z_STREAM_ERROR$1; }
    state = strm.state;

    if (state.wrap !== 0 && state.mode !== DICT) {
      return Z_STREAM_ERROR$1;
    }

    /* check for correct dictionary identifier */
    if (state.mode === DICT) {
      dictid = 1; /* adler32(0, null, 0)*/
      /* dictid = adler32(dictid, dictionary, dictLength); */
      dictid = adler32_1(dictid, dictionary, dictLength, 0);
      if (dictid !== state.check) {
        return Z_DATA_ERROR$1;
      }
    }
    /* copy dictionary to window using updatewindow(), which will amend the
     existing dictionary if appropriate */
    ret = updatewindow(strm, dictionary, dictLength, dictLength);
    if (ret) {
      state.mode = MEM;
      return Z_MEM_ERROR$1;
    }
    state.havedict = 1;
    // Tracev((stderr, "inflate:   dictionary set\n"));
    return Z_OK$1;
  };


  var inflateReset_1 = inflateReset;
  var inflateReset2_1 = inflateReset2;
  var inflateResetKeep_1 = inflateResetKeep;
  var inflateInit_1 = inflateInit;
  var inflateInit2_1 = inflateInit2;
  var inflate_2$1 = inflate$2;
  var inflateEnd_1 = inflateEnd;
  var inflateGetHeader_1 = inflateGetHeader;
  var inflateSetDictionary_1 = inflateSetDictionary;
  var inflateInfo = 'pako inflate (from Nodeca project)';

  /* Not implemented
  module.exports.inflateCodesUsed = inflateCodesUsed;
  module.exports.inflateCopy = inflateCopy;
  module.exports.inflateGetDictionary = inflateGetDictionary;
  module.exports.inflateMark = inflateMark;
  module.exports.inflatePrime = inflatePrime;
  module.exports.inflateSync = inflateSync;
  module.exports.inflateSyncPoint = inflateSyncPoint;
  module.exports.inflateUndermine = inflateUndermine;
  module.exports.inflateValidate = inflateValidate;
  */

  var inflate_1$2 = {
  	inflateReset: inflateReset_1,
  	inflateReset2: inflateReset2_1,
  	inflateResetKeep: inflateResetKeep_1,
  	inflateInit: inflateInit_1,
  	inflateInit2: inflateInit2_1,
  	inflate: inflate_2$1,
  	inflateEnd: inflateEnd_1,
  	inflateGetHeader: inflateGetHeader_1,
  	inflateSetDictionary: inflateSetDictionary_1,
  	inflateInfo: inflateInfo
  };

  // (C) 1995-2013 Jean-loup Gailly and Mark Adler
  // (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
  //
  // This software is provided 'as-is', without any express or implied
  // warranty. In no event will the authors be held liable for any damages
  // arising from the use of this software.
  //
  // Permission is granted to anyone to use this software for any purpose,
  // including commercial applications, and to alter it and redistribute it
  // freely, subject to the following restrictions:
  //
  // 1. The origin of this software must not be misrepresented; you must not
  //   claim that you wrote the original software. If you use this software
  //   in a product, an acknowledgment in the product documentation would be
  //   appreciated but is not required.
  // 2. Altered source versions must be plainly marked as such, and must not be
  //   misrepresented as being the original software.
  // 3. This notice may not be removed or altered from any source distribution.

  function GZheader() {
    /* true if compressed data believed to be text */
    this.text       = 0;
    /* modification time */
    this.time       = 0;
    /* extra flags (not used when writing a gzip file) */
    this.xflags     = 0;
    /* operating system */
    this.os         = 0;
    /* pointer to extra field or Z_NULL if none */
    this.extra      = null;
    /* extra field length (valid if extra != Z_NULL) */
    this.extra_len  = 0; // Actually, we don't need it in JS,
                         // but leave for few code modifications

    //
    // Setup limits is not necessary because in js we should not preallocate memory
    // for inflate use constant limit in 65536 bytes
    //

    /* space at extra (only when reading header) */
    // this.extra_max  = 0;
    /* pointer to zero-terminated file name or Z_NULL */
    this.name       = '';
    /* space at name (only when reading header) */
    // this.name_max   = 0;
    /* pointer to zero-terminated comment or Z_NULL */
    this.comment    = '';
    /* space at comment (only when reading header) */
    // this.comm_max   = 0;
    /* true if there was or will be a header crc */
    this.hcrc       = 0;
    /* true when done reading gzip header (not used when writing a gzip file) */
    this.done       = false;
  }

  var gzheader = GZheader;

  const toString = Object.prototype.toString;

  /* Public constants ==========================================================*/
  /* ===========================================================================*/

  const {
    Z_NO_FLUSH, Z_FINISH,
    Z_OK, Z_STREAM_END, Z_NEED_DICT, Z_STREAM_ERROR, Z_DATA_ERROR, Z_MEM_ERROR
  } = constants$2;

  /* ===========================================================================*/


  /**
   * class Inflate
   *
   * Generic JS-style wrapper for zlib calls. If you don't need
   * streaming behaviour - use more simple functions: [[inflate]]
   * and [[inflateRaw]].
   **/

  /* internal
   * inflate.chunks -> Array
   *
   * Chunks of output data, if [[Inflate#onData]] not overridden.
   **/

  /**
   * Inflate.result -> Uint8Array|String
   *
   * Uncompressed result, generated by default [[Inflate#onData]]
   * and [[Inflate#onEnd]] handlers. Filled after you push last chunk
   * (call [[Inflate#push]] with `Z_FINISH` / `true` param).
   **/

  /**
   * Inflate.err -> Number
   *
   * Error code after inflate finished. 0 (Z_OK) on success.
   * Should be checked if broken data possible.
   **/

  /**
   * Inflate.msg -> String
   *
   * Error message, if [[Inflate.err]] != 0
   **/


  /**
   * new Inflate(options)
   * - options (Object): zlib inflate options.
   *
   * Creates new inflator instance with specified params. Throws exception
   * on bad params. Supported options:
   *
   * - `windowBits`
   * - `dictionary`
   *
   * [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced)
   * for more information on these.
   *
   * Additional options, for internal needs:
   *
   * - `chunkSize` - size of generated data chunks (16K by default)
   * - `raw` (Boolean) - do raw inflate
   * - `to` (String) - if equal to 'string', then result will be converted
   *   from utf8 to utf16 (javascript) string. When string output requested,
   *   chunk length can differ from `chunkSize`, depending on content.
   *
   * By default, when no options set, autodetect deflate/gzip data format via
   * wrapper header.
   *
   * ##### Example:
   *
   * ```javascript
   * const pako = require('pako')
   * const chunk1 = new Uint8Array([1,2,3,4,5,6,7,8,9])
   * const chunk2 = new Uint8Array([10,11,12,13,14,15,16,17,18,19]);
   *
   * const inflate = new pako.Inflate({ level: 3});
   *
   * inflate.push(chunk1, false);
   * inflate.push(chunk2, true);  // true -> last chunk
   *
   * if (inflate.err) { throw new Error(inflate.err); }
   *
   * console.log(inflate.result);
   * ```
   **/
  function Inflate$1(options) {
    this.options = common.assign({
      chunkSize: 1024 * 64,
      windowBits: 15,
      to: ''
    }, options || {});

    const opt = this.options;

    // Force window size for `raw` data, if not set directly,
    // because we have no header for autodetect.
    if (opt.raw && (opt.windowBits >= 0) && (opt.windowBits < 16)) {
      opt.windowBits = -opt.windowBits;
      if (opt.windowBits === 0) { opt.windowBits = -15; }
    }

    // If `windowBits` not defined (and mode not raw) - set autodetect flag for gzip/deflate
    if ((opt.windowBits >= 0) && (opt.windowBits < 16) &&
        !(options && options.windowBits)) {
      opt.windowBits += 32;
    }

    // Gzip header has no info about windows size, we can do autodetect only
    // for deflate. So, if window size not set, force it to max when gzip possible
    if ((opt.windowBits > 15) && (opt.windowBits < 48)) {
      // bit 3 (16) -> gzipped data
      // bit 4 (32) -> autodetect gzip/deflate
      if ((opt.windowBits & 15) === 0) {
        opt.windowBits |= 15;
      }
    }

    this.err    = 0;      // error code, if happens (0 = Z_OK)
    this.msg    = '';     // error message
    this.ended  = false;  // used to avoid multiple onEnd() calls
    this.chunks = [];     // chunks of compressed data

    this.strm   = new zstream();
    this.strm.avail_out = 0;

    let status  = inflate_1$2.inflateInit2(
      this.strm,
      opt.windowBits
    );

    if (status !== Z_OK) {
      throw new Error(messages[status]);
    }

    this.header = new gzheader();

    inflate_1$2.inflateGetHeader(this.strm, this.header);

    // Setup dictionary
    if (opt.dictionary) {
      // Convert data if needed
      if (typeof opt.dictionary === 'string') {
        opt.dictionary = strings.string2buf(opt.dictionary);
      } else if (toString.call(opt.dictionary) === '[object ArrayBuffer]') {
        opt.dictionary = new Uint8Array(opt.dictionary);
      }
      if (opt.raw) { //In raw mode we need to set the dictionary early
        status = inflate_1$2.inflateSetDictionary(this.strm, opt.dictionary);
        if (status !== Z_OK) {
          throw new Error(messages[status]);
        }
      }
    }
  }

  /**
   * Inflate#push(data[, flush_mode]) -> Boolean
   * - data (Uint8Array|ArrayBuffer): input data
   * - flush_mode (Number|Boolean): 0..6 for corresponding Z_NO_FLUSH..Z_TREE
   *   flush modes. See constants. Skipped or `false` means Z_NO_FLUSH,
   *   `true` means Z_FINISH.
   *
   * Sends input data to inflate pipe, generating [[Inflate#onData]] calls with
   * new output chunks. Returns `true` on success. If end of stream detected,
   * [[Inflate#onEnd]] will be called.
   *
   * `flush_mode` is not needed for normal operation, because end of stream
   * detected automatically. You may try to use it for advanced things, but
   * this functionality was not tested.
   *
   * On fail call [[Inflate#onEnd]] with error code and return false.
   *
   * ##### Example
   *
   * ```javascript
   * push(chunk, false); // push one of data chunks
   * ...
   * push(chunk, true);  // push last chunk
   * ```
   **/
  Inflate$1.prototype.push = function (data, flush_mode) {
    const strm = this.strm;
    const chunkSize = this.options.chunkSize;
    const dictionary = this.options.dictionary;
    let status, _flush_mode, last_avail_out;

    if (this.ended) return false;

    if (flush_mode === ~~flush_mode) _flush_mode = flush_mode;
    else _flush_mode = flush_mode === true ? Z_FINISH : Z_NO_FLUSH;

    // Convert data if needed
    if (toString.call(data) === '[object ArrayBuffer]') {
      strm.input = new Uint8Array(data);
    } else {
      strm.input = data;
    }

    strm.next_in = 0;
    strm.avail_in = strm.input.length;

    for (;;) {
      if (strm.avail_out === 0) {
        strm.output = new Uint8Array(chunkSize);
        strm.next_out = 0;
        strm.avail_out = chunkSize;
      }

      status = inflate_1$2.inflate(strm, _flush_mode);

      if (status === Z_NEED_DICT && dictionary) {
        status = inflate_1$2.inflateSetDictionary(strm, dictionary);

        if (status === Z_OK) {
          status = inflate_1$2.inflate(strm, _flush_mode);
        } else if (status === Z_DATA_ERROR) {
          // Replace code with more verbose
          status = Z_NEED_DICT;
        }
      }

      // Skip snyc markers if more data follows and not raw mode
      while (strm.avail_in > 0 &&
             status === Z_STREAM_END &&
             strm.state.wrap > 0 &&
             data[strm.next_in] !== 0)
      {
        inflate_1$2.inflateReset(strm);
        status = inflate_1$2.inflate(strm, _flush_mode);
      }

      switch (status) {
        case Z_STREAM_ERROR:
        case Z_DATA_ERROR:
        case Z_NEED_DICT:
        case Z_MEM_ERROR:
          this.onEnd(status);
          this.ended = true;
          return false;
      }

      // Remember real `avail_out` value, because we may patch out buffer content
      // to align utf8 strings boundaries.
      last_avail_out = strm.avail_out;

      if (strm.next_out) {
        if (strm.avail_out === 0 || status === Z_STREAM_END) {

          if (this.options.to === 'string') {

            let next_out_utf8 = strings.utf8border(strm.output, strm.next_out);

            let tail = strm.next_out - next_out_utf8;
            let utf8str = strings.buf2string(strm.output, next_out_utf8);

            // move tail & realign counters
            strm.next_out = tail;
            strm.avail_out = chunkSize - tail;
            if (tail) strm.output.set(strm.output.subarray(next_out_utf8, next_out_utf8 + tail), 0);

            this.onData(utf8str);

          } else {
            this.onData(strm.output.length === strm.next_out ? strm.output : strm.output.subarray(0, strm.next_out));
          }
        }
      }

      // Must repeat iteration if out buffer is full
      if (status === Z_OK && last_avail_out === 0) continue;

      // Finalize if end of stream reached.
      if (status === Z_STREAM_END) {
        status = inflate_1$2.inflateEnd(this.strm);
        this.onEnd(status);
        this.ended = true;
        return true;
      }

      if (strm.avail_in === 0) break;
    }

    return true;
  };


  /**
   * Inflate#onData(chunk) -> Void
   * - chunk (Uint8Array|String): output data. When string output requested,
   *   each chunk will be string.
   *
   * By default, stores data blocks in `chunks[]` property and glue
   * those in `onEnd`. Override this handler, if you need another behaviour.
   **/
  Inflate$1.prototype.onData = function (chunk) {
    this.chunks.push(chunk);
  };


  /**
   * Inflate#onEnd(status) -> Void
   * - status (Number): inflate status. 0 (Z_OK) on success,
   *   other if not.
   *
   * Called either after you tell inflate that the input stream is
   * complete (Z_FINISH). By default - join collected chunks,
   * free memory and fill `results` / `err` properties.
   **/
  Inflate$1.prototype.onEnd = function (status) {
    // On success - join
    if (status === Z_OK) {
      if (this.options.to === 'string') {
        this.result = this.chunks.join('');
      } else {
        this.result = common.flattenChunks(this.chunks);
      }
    }
    this.chunks = [];
    this.err = status;
    this.msg = this.strm.msg;
  };


  /**
   * inflate(data[, options]) -> Uint8Array|String
   * - data (Uint8Array|ArrayBuffer): input data to decompress.
   * - options (Object): zlib inflate options.
   *
   * Decompress `data` with inflate/ungzip and `options`. Autodetect
   * format via wrapper header by default. That's why we don't provide
   * separate `ungzip` method.
   *
   * Supported options are:
   *
   * - windowBits
   *
   * [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced)
   * for more information.
   *
   * Sugar (options):
   *
   * - `raw` (Boolean) - say that we work with raw stream, if you don't wish to specify
   *   negative windowBits implicitly.
   * - `to` (String) - if equal to 'string', then result will be converted
   *   from utf8 to utf16 (javascript) string. When string output requested,
   *   chunk length can differ from `chunkSize`, depending on content.
   *
   *
   * ##### Example:
   *
   * ```javascript
   * const pako = require('pako');
   * const input = pako.deflate(new Uint8Array([1,2,3,4,5,6,7,8,9]));
   * let output;
   *
   * try {
   *   output = pako.inflate(input);
   * } catch (err) {
   *   console.log(err);
   * }
   * ```
   **/
  function inflate$1(input, options) {
    const inflator = new Inflate$1(options);

    inflator.push(input);

    // That will never happens, if you don't cheat with options :)
    if (inflator.err) throw inflator.msg || messages[inflator.err];

    return inflator.result;
  }
  var inflate_2 = inflate$1;

  var inflate_1$1 = {
  	inflate: inflate_2};

  const { inflate} = inflate_1$1;
  var inflate_1 = inflate;

  // Mocking the pako module to just have inflate for a smaller package size
  const pako = { inflate: inflate_1 };

  var Typr = {};

  Typr["parse"] = function (buff) {
    var bin = Typr["B"];

    var readFont = function (data, idx, offset, tmap) {
      var T = Typr["T"];
      var prsr = {
        "cmap": T.cmap,
        "head": T.head,
        "hhea": T.hhea,
        "maxp": T.maxp,
        "hmtx": T.hmtx,
        "name": T.name,
        "OS/2": T.OS2,
        "post": T.post,

        "loca": T.loca,
        "kern": T.kern,
        "glyf": T.glyf,

        "CFF ": T.CFF,
        /*
        "GPOS",
        "GSUB",
        "GDEF",*/
        "GSUB": T.GSUB,
        "CBLC": T.CBLC,
        "CBDT": T.CBDT,

        "SVG ": T.SVG,
        "COLR": T.colr,
        "CPAL": T.cpal,
        "sbix": T.sbix,

        "fvar": T.fvar,
        "gvar": T.gvar,
        "avar": T.avar,
        "HVAR": T.HVAR
        //"VORG",
      };
      var obj = { "_data": data, "_index": idx, "_offset": offset };

      for (var t in prsr) {
        var tab = Typr["findTable"](data, t, offset);
        if (tab) {
          var off = tab[0], tobj = tmap[off];
          if (tobj == null) tobj = prsr[t].parseTab(data, off, tab[1], obj);
          obj[t] = tmap[off] = tobj;
        }
      }
      return obj;
    };

    function woffToOtf(data) {
      var numTables = bin.readUshort(data, 12);
      var totalSize = bin.readUint(data, 16);

      var otf = new Uint8Array(totalSize), toff = 12 + numTables * 16;

      bin.writeASCII(otf, 0, "OTTO");
      bin.writeUshort(otf, 4, numTables);

      var off = 44;
      for (var i = 0; i < numTables; i++) {
        var tag = bin.readASCII(data, off, 4);
        var tof = bin.readUint(data, off + 4);
        var cLe = bin.readUint(data, off + 8);
        var oLe = bin.readUint(data, off + 12);
        off += 20;
        //console.log(i, ":::", tag,tof,oLe);

        var tab = data.slice(tof, tof + cLe);
        if (cLe != oLe) tab = pako["inflate"](tab);

        var to = 12 + i * 16;
        bin.writeASCII(otf, to, tag);
        bin.writeUint(otf, to + 8, toff);
        bin.writeUint(otf, to + 12, oLe);

        otf.set(tab, toff); toff += oLe;
      }
      //console.log(otf);
      return otf;
    }


    var data = new Uint8Array(buff);
    // PATCHED: keep around the compressed data if we inflate it
    let compressedData;
    if (data[0] == 0x77) {
      compressedData = data;
      data = woffToOtf(data);
    }

    var tmap = {};
    var tag = bin.readASCII(data, 0, 4);
    if (tag == "ttcf") {
      var offset = 4;
      bin.readUshort(data, offset); offset += 2;
      bin.readUshort(data, offset); offset += 2;
      var numF = bin.readUint(data, offset); offset += 4;
      var fnts = [];
      for (var i = 0; i < numF; i++) {
        var foff = bin.readUint(data, offset); offset += 4;
        fnts.push(readFont(data, i, foff, tmap));
      }
      return fnts;
    }
    var fnt = readFont(data, 0, 0, tmap);  //console.log(fnt);  throw "e";
    fnt._compressedData = compressedData; // PATCH: make compressed data accessible
    var fvar = fnt["fvar"];
    if (fvar) {
      var out = [fnt];
      for (var i = 0; i < fvar[1].length; i++) {
        var fv = fvar[1][i];
        var obj = {}; out.push(obj); for (var p in fnt) obj[p] = fnt[p];
        obj["_index"] = i;
        var name = obj["name"] = JSON.parse(JSON.stringify(obj["name"]));
        name["fontSubfamily"] = fv[0];
        if (fv[3] == null) fv[3] = (name["fontFamily"] + "-" + name["fontSubfamily"])["replaceAll"](" ", "");
        name["postScriptName"] = fv[3];
      }
      return out;
    }

    return [fnt];
  };


  Typr["findTable"] = function (data, tab, foff) {
    var bin = Typr["B"];
    var numTables = bin.readUshort(data, foff + 4);
    var offset = foff + 12;
    for (var i = 0; i < numTables; i++) {
      var tag = bin.readASCII(data, offset, 4);   //console.log(tag);
      bin.readUint(data, offset + 4);
      var toffset = bin.readUint(data, offset + 8);
      var length = bin.readUint(data, offset + 12);
      if (tag == tab) return [toffset, length];
      offset += 16;
    }
    return null;
  };
  /*
  Typr["splitBy"] = function(data,tag) {
    data = new Uint8Array(data);  console.log(data.slice(0,64));
    var bin = Typr["B"];
    var ttcf = bin.readASCII(data, 0, 4);  if(ttcf!="ttcf") return {};

    var offset = 8;
    var numF = bin.readUint  (data, offset);  offset+=4;
    var colls = [], used={};
    for(var i=0; i<numF; i++) {
      var foff = bin.readUint  (data, offset);  offset+=4;
      var toff = Typr["findTable"](data,tag,foff)[0];
      if(used[toff]==null) used[toff] = [];
      used[toff].push([foff,bin.readUshort(data,foff+4)]);  // font offset, numTables
    }
    for(var toff in used) {
      var offs = used[toff];
      var hlen = 12+4*offs.length;
      var out = new Uint8Array(hlen);
      for(var i=0; i<8; i++) out[i]=data[i];
      bin.writeUint(out,8,offs.length);

      for(var i=0; i<offs.length; i++) hlen += 12+offs[i][1]*16;

      var hdrs = [out], tabs = [], hoff=out.length, toff=hlen, noffs={};
      for(var i=0; i<offs.length; i++) {
        bin.writeUint(out, 12+i*4, hoff);  hoff+=12+offs[i][1]*16;
        toff = Typr["_cutFont"](data, offs[i][0], hdrs, tabs, toff, noffs);
      }
      colls.push(Typr["_joinArrs"](hdrs.concat(tabs)));
    }
    return colls;
  }

  Typr["splitFonts"] = function(data) {
    data = new Uint8Array(data);
    var bin = Typr["B"];
    var ttcf = bin.readASCII(data, 0, 4);  if(ttcf!="ttcf") return {};

    var offset = 8;
    var numF = bin.readUint  (data, offset);  offset+=4;
    var fnts = [];
    for(var i=0; i<numF; i++) {
      var foff = bin.readUint  (data, offset);  offset+=4;
      fnts.push(Typr._cutFont(data, foff));
      break;
    }
    return fnts;
  }

  Typr["_cutFont"] = function(data,foff,hdrs,tabs,toff, noffs) {
    var bin = Typr["B"];
    var numTables = bin.readUshort(data, foff+4);

    var out = new Uint8Array(12+numTables*16);  hdrs.push(out);
    for(var i=0; i<12; i++) out[i]=data[foff+i];  //console.log(out);

    var off = 12;
    for(var i=0; i<numTables; i++)
    {
      var tag      = bin.readASCII(data, foff+off, 4);
      var checkSum = bin.readUint (data, foff+off+ 4);
      var toffset  = bin.readUint (data, foff+off+ 8);
      var length   = bin.readUint (data, foff+off+12);

      while((length&3)!=0) length++;

      for(var j=0; j<16; j++) out[off+j]=data[foff+off+j];

      if(noffs[toffset]!=null) bin.writeUint(out,off+8,noffs[toffset]);
      else {
        noffs[toffset] = toff;
        bin.writeUint(out, off+8, toff);
        tabs.push(new Uint8Array(data.buffer, toffset, length));  toff+=length;
      }
      off+=16;
    }
    return toff;
  }
  Typr["_joinArrs"] = function(tabs) {
    var len = 0;
    for(var i=0; i<tabs.length; i++) len+=tabs[i].length;
    var out = new Uint8Array(len), ooff=0;
    for(var i=0; i<tabs.length; i++) {
      var tab = tabs[i];
      for(var j=0; j<tab.length; j++) out[ooff+j]=tab[j];
      ooff+=tab.length;
    }
    return out;
  }
  */

  Typr["T"] = {};





  Typr["B"] = {
    readFixed: function (data, o) {
      return ((data[o] << 8) | data[o + 1]) + (((data[o + 2] << 8) | data[o + 3]) / (256 * 256 + 4));
    },
    readF2dot14: function (data, o) {
      var num = Typr["B"].readShort(data, o);
      return num / 16384;
    },
    readInt: function (buff, p) {
      //if(p>=buff.length) throw "error";
      var a = Typr["B"].t.uint8;
      a[0] = buff[p + 3];
      a[1] = buff[p + 2];
      a[2] = buff[p + 1];
      a[3] = buff[p];
      return Typr["B"].t.int32[0];
    },

    readInt8: function (buff, p) {
      //if(p>=buff.length) throw "error";
      var a = Typr["B"].t.uint8;
      a[0] = buff[p];
      return Typr["B"].t.int8[0];
    },
    readShort: function (buff, p) {
      //if(p>=buff.length) throw "error";
      var a = Typr["B"].t.uint16;
      a[0] = (buff[p] << 8) | buff[p + 1];
      return Typr["B"].t.int16[0];
    },
    readUshort: function (buff, p) {
      //if(p>=buff.length) throw "error";
      return (buff[p] << 8) | buff[p + 1];
    },
    writeUshort: function (buff, p, n) {
      buff[p] = (n >> 8) & 255; buff[p + 1] = n & 255;
    },
    readUshorts: function (buff, p, len) {
      var arr = [];
      for (var i = 0; i < len; i++) {
        var v = Typr["B"].readUshort(buff, p + i * 2);  //if(v==932) console.log(p+i*2);
        arr.push(v);
      }
      return arr;
    },
    readUint: function (buff, p) {
      //if(p>=buff.length) throw "error";
      var a = Typr["B"].t.uint8;
      a[3] = buff[p]; a[2] = buff[p + 1]; a[1] = buff[p + 2]; a[0] = buff[p + 3];
      return Typr["B"].t.uint32[0];
    },
    writeUint: function (buff, p, n) {
      buff[p] = (n >> 24) & 255; buff[p + 1] = (n >> 16) & 255; buff[p + 2] = (n >> 8) & 255; buff[p + 3] = (n >> 0) & 255;
    },
    readUint64: function (buff, p) {
      //if(p>=buff.length) throw "error";
      return (Typr["B"].readUint(buff, p) * (0xffffffff + 1)) + Typr["B"].readUint(buff, p + 4);
    },
    readASCII: function (buff, p, l)	// l : length in Characters (not Bytes)
    {
      //if(p>=buff.length) throw "error";
      var s = "";
      for (var i = 0; i < l; i++) s += String.fromCharCode(buff[p + i]);
      return s;
    },
    writeASCII: function (buff, p, s)	// l : length in Characters (not Bytes)
    {
      for (var i = 0; i < s.length; i++)
        buff[p + i] = s.charCodeAt(i);
    },
    readUnicode: function (buff, p, l) {
      //if(p>=buff.length) throw "error";
      var s = "";
      for (var i = 0; i < l; i++) {
        var c = (buff[p++] << 8) | buff[p++];
        s += String.fromCharCode(c);
      }
      return s;
    },
    _tdec: window["TextDecoder"] ? new window["TextDecoder"]() : null,
    readUTF8: function (buff, p, l) {
      var tdec = Typr["B"]._tdec;
      if (tdec && p == 0 && l == buff.length) return tdec["decode"](buff);
      return Typr["B"].readASCII(buff, p, l);
    },
    readBytes: function (buff, p, l) {
      //if(p>=buff.length) throw "error";
      var arr = [];
      for (var i = 0; i < l; i++) arr.push(buff[p + i]);
      return arr;
    },
    readASCIIArray: function (buff, p, l)	// l : length in Characters (not Bytes)
    {
      //if(p>=buff.length) throw "error";
      var s = [];
      for (var i = 0; i < l; i++)
        s.push(String.fromCharCode(buff[p + i]));
      return s;
    },
    t: function () {
      var ab = new ArrayBuffer(8);
      return {
        buff: ab,
        int8: new Int8Array(ab),
        uint8: new Uint8Array(ab),
        int16: new Int16Array(ab),
        uint16: new Uint16Array(ab),
        int32: new Int32Array(ab),
        uint32: new Uint32Array(ab)
      }
    }()
  };






  Typr["T"].CFF = {
    parseTab: function (data, offset, length) {
      var bin = Typr["B"];
      var CFF = Typr["T"].CFF;

      data = new Uint8Array(data.buffer, offset, length);
      offset = 0;
   offset++;
   offset++;
   offset++;
   offset++;
      //console.log(major, minor, hdrSize, offsize);

      // Name INDEX
      var ninds = [];
      offset = CFF.readIndex(data, offset, ninds);
      var names = [];

      for (var i = 0; i < ninds.length - 1; i++) names.push(bin.readASCII(data, offset + ninds[i], ninds[i + 1] - ninds[i]));
      offset += ninds[ninds.length - 1];


      // Top DICT INDEX
      var tdinds = [];
      offset = CFF.readIndex(data, offset, tdinds);  //console.log(tdinds);
      // Top DICT Data
      var topDicts = [];
      for (var i = 0; i < tdinds.length - 1; i++) topDicts.push(CFF.readDict(data, offset + tdinds[i], offset + tdinds[i + 1]));
      offset += tdinds[tdinds.length - 1];
      var topdict = topDicts[0];
      //console.log(topdict);

      // String INDEX
      var sinds = [];
      offset = CFF.readIndex(data, offset, sinds);
      // String Data
      var strings = [];
      for (var i = 0; i < sinds.length - 1; i++) strings.push(bin.readASCII(data, offset + sinds[i], sinds[i + 1] - sinds[i]));
      offset += sinds[sinds.length - 1];

      // Global Subr INDEX  (subroutines)
      CFF.readSubrs(data, offset, topdict);

      // charstrings

      if (topdict["CharStrings"]) topdict["CharStrings"] = CFF.readBytes(data, topdict["CharStrings"]);

      // CID font
      if (topdict["ROS"]) {
        offset = topdict["FDArray"];
        var fdind = [];
        offset = CFF.readIndex(data, offset, fdind);

        topdict["FDArray"] = [];
        for (var i = 0; i < fdind.length - 1; i++) {
          var dict = CFF.readDict(data, offset + fdind[i], offset + fdind[i + 1]);
          CFF._readFDict(data, dict, strings);
          topdict["FDArray"].push(dict);
        }
        offset += fdind[fdind.length - 1];

        offset = topdict["FDSelect"];
        topdict["FDSelect"] = [];
        var fmt = data[offset]; offset++;
        if (fmt == 3) {
          var rns = bin.readUshort(data, offset); offset += 2;
          for (var i = 0; i < rns + 1; i++) {
            topdict["FDSelect"].push(bin.readUshort(data, offset), data[offset + 2]); offset += 3;
          }
        }
        else throw fmt;
      }

      // Encoding
      //if(topdict["Encoding"]) topdict["Encoding"] = CFF.readEncoding(data, topdict["Encoding"], topdict["CharStrings"].length);

      // charset
      if (topdict["charset"]) topdict["charset"] = CFF.readCharset(data, topdict["charset"], topdict["CharStrings"].length);

      CFF._readFDict(data, topdict, strings);
      return topdict;
    },

    _readFDict: function (data, dict, ss) {
      var CFF = Typr["T"].CFF;
      var offset;
      if (dict["Private"]) {
        offset = dict["Private"][1];
        dict["Private"] = CFF.readDict(data, offset, offset + dict["Private"][0]);
        if (dict["Private"]["Subrs"]) CFF.readSubrs(data, offset + dict["Private"]["Subrs"], dict["Private"]);
      }
      for (var p in dict) if (["FamilyName", "FontName", "FullName", "Notice", "version", "Copyright"].indexOf(p) != -1) dict[p] = ss[dict[p] - 426 + 35];
    },

    readSubrs: function (data, offset, obj) {
      obj["Subrs"] = Typr["T"].CFF.readBytes(data, offset);

      var bias, nSubrs = obj["Subrs"].length + 1;
      if (nSubrs < 1240) bias = 107;
      else if (nSubrs < 33900) bias = 1131;
      else bias = 32768;
      obj["Bias"] = bias;
    },
    readBytes: function (data, offset) {
      var arr = [];
      offset = Typr["T"].CFF.readIndex(data, offset, arr);

      var subrs = [], arl = arr.length - 1, no = data.byteOffset + offset;
      for (var i = 0; i < arl; i++) {
        var ari = arr[i];
        subrs.push(new Uint8Array(data.buffer, no + ari, arr[i + 1] - ari));
      }
      return subrs;
    },

    tableSE: [
      0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0,
      1, 2, 3, 4, 5, 6, 7, 8,
      9, 10, 11, 12, 13, 14, 15, 16,
      17, 18, 19, 20, 21, 22, 23, 24,
      25, 26, 27, 28, 29, 30, 31, 32,
      33, 34, 35, 36, 37, 38, 39, 40,
      41, 42, 43, 44, 45, 46, 47, 48,
      49, 50, 51, 52, 53, 54, 55, 56,
      57, 58, 59, 60, 61, 62, 63, 64,
      65, 66, 67, 68, 69, 70, 71, 72,
      73, 74, 75, 76, 77, 78, 79, 80,
      81, 82, 83, 84, 85, 86, 87, 88,
      89, 90, 91, 92, 93, 94, 95, 0,
      0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0,
      0, 96, 97, 98, 99, 100, 101, 102,
      103, 104, 105, 106, 107, 108, 109, 110,
      0, 111, 112, 113, 114, 0, 115, 116,
      117, 118, 119, 120, 121, 122, 0, 123,
      0, 124, 125, 126, 127, 128, 129, 130,
      131, 0, 132, 133, 0, 134, 135, 136,
      137, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0,
      0, 138, 0, 139, 0, 0, 0, 0,
      140, 141, 142, 143, 0, 0, 0, 0,
      0, 144, 0, 0, 0, 145, 0, 0,
      146, 147, 148, 149, 0, 0, 0, 0
    ],

    glyphByUnicode: function (cff, code) {
      for (var i = 0; i < cff["charset"].length; i++) if (cff["charset"][i] == code) return i;
      return -1;
    },

    glyphBySE: function (cff, charcode)	// glyph by standard encoding
    {
      if (charcode < 0 || charcode > 255) return -1;
      return Typr["T"].CFF.glyphByUnicode(cff, Typr["T"].CFF.tableSE[charcode]);
    },

    /*readEncoding : function(data, offset, num)
    {
      var bin = Typr["B"];

      var array = ['.notdef'];
      var format = data[offset];  offset++;
      //console.log("Encoding");
      //console.log(format);

      if(format==0)
      {
        var nCodes = data[offset];  offset++;
        for(var i=0; i<nCodes; i++)  array.push(data[offset+i]);
      }
      /*
      else if(format==1 || format==2)
      {
        while(charset.length<num)
        {
          var first = bin.readUshort(data, offset);  offset+=2;
          var nLeft=0;
          if(format==1) {  nLeft = data[offset];  offset++;  }
          else          {  nLeft = bin.readUshort(data, offset);  offset+=2;  }
          for(var i=0; i<=nLeft; i++)  {  charset.push(first);  first++;  }
        }
      }

      else throw "error: unknown encoding format: " + format;

      return array;
    },*/

    readCharset: function (data, offset, num) {
      var bin = Typr["B"];

      var charset = ['.notdef'];
      var format = data[offset]; offset++;

      if (format == 0) {
        for (var i = 0; i < num; i++) {
          var first = bin.readUshort(data, offset); offset += 2;
          charset.push(first);
        }
      }
      else if (format == 1 || format == 2) {
        while (charset.length < num) {
          var first = bin.readUshort(data, offset); offset += 2;
          var nLeft = 0;
          if (format == 1) { nLeft = data[offset]; offset++; }
          else { nLeft = bin.readUshort(data, offset); offset += 2; }
          for (var i = 0; i <= nLeft; i++) { charset.push(first); first++; }
        }
      }
      else throw "error: format: " + format;

      return charset;
    },

    readIndex: function (data, offset, inds) {
      var bin = Typr["B"];

      var count = bin.readUshort(data, offset) + 1; offset += 2;
      var offsize = data[offset]; offset++;

      if (offsize == 1) for (var i = 0; i < count; i++) inds.push(data[offset + i]);
      else if (offsize == 2) for (var i = 0; i < count; i++) inds.push(bin.readUshort(data, offset + i * 2));
      else if (offsize == 3) for (var i = 0; i < count; i++) inds.push(bin.readUint(data, offset + i * 3 - 1) & 0x00ffffff);
      else if (offsize == 4) for (var i = 0; i < count; i++) inds.push(bin.readUint(data, offset + i * 4));
      else if (count != 1) throw "unsupported offset size: " + offsize + ", count: " + count;

      offset += count * offsize;
      return offset - 1;
    },

    getCharString: function (data, offset, o) {
      var bin = Typr["B"];

      var b0 = data[offset], b1 = data[offset + 1];
      var vs = 1;
      var op = null, val = null;
      // operand
      if (b0 <= 20) { op = b0; vs = 1; }
      if (b0 == 12) { op = b0 * 100 + b1; vs = 2; }
      //if(b0==19 || b0==20) { op = b0/*+" "+b1*/;  vs=2; }
      if (21 <= b0 && b0 <= 27) { op = b0; vs = 1; }
      if (b0 == 28) { val = bin.readShort(data, offset + 1); vs = 3; }
      if (29 <= b0 && b0 <= 31) { op = b0; vs = 1; }
      if (32 <= b0 && b0 <= 246) { val = b0 - 139; vs = 1; }
      if (247 <= b0 && b0 <= 250) { val = (b0 - 247) * 256 + b1 + 108; vs = 2; }
      if (251 <= b0 && b0 <= 254) { val = -(b0 - 251) * 256 - b1 - 108; vs = 2; }
      if (b0 == 255) { val = bin.readInt(data, offset + 1) / 0xffff; vs = 5; }

      o.val = val != null ? val : "o" + op;
      o.size = vs;
    },

    readCharString: function (data, offset, length) {
      var end = offset + length;
      var bin = Typr["B"];
      var arr = [];

      while (offset < end) {
        var b0 = data[offset], b1 = data[offset + 1];
        var vs = 1;
        var op = null, val = null;
        // operand
        if (b0 <= 20) { op = b0; vs = 1; }
        if (b0 == 12) { op = b0 * 100 + b1; vs = 2; }
        if (b0 == 19 || b0 == 20) { op = b0/*+" "+b1*/; vs = 2; }
        if (21 <= b0 && b0 <= 27) { op = b0; vs = 1; }
        if (b0 == 28) { val = bin.readShort(data, offset + 1); vs = 3; }
        if (29 <= b0 && b0 <= 31) { op = b0; vs = 1; }
        if (32 <= b0 && b0 <= 246) { val = b0 - 139; vs = 1; }
        if (247 <= b0 && b0 <= 250) { val = (b0 - 247) * 256 + b1 + 108; vs = 2; }
        if (251 <= b0 && b0 <= 254) { val = -(b0 - 251) * 256 - b1 - 108; vs = 2; }
        if (b0 == 255) { val = bin.readInt(data, offset + 1) / 0xffff; vs = 5; }

        arr.push(val != null ? val : "o" + op);
        offset += vs;

        //var cv = arr[arr.length-1];
        //if(cv==undefined) throw "error";
        //console.log()
      }
      return arr;
    },

    readDict: function (data, offset, end) {
      var bin = Typr["B"];
      //var dict = [];
      var dict = {};
      var carr = [];

      while (offset < end) {
        var b0 = data[offset], b1 = data[offset + 1];
        var vs = 1;
        var key = null, val = null;
        // operand
        if (b0 == 28) { val = bin.readShort(data, offset + 1); vs = 3; }
        if (b0 == 29) { val = bin.readInt(data, offset + 1); vs = 5; }
        if (32 <= b0 && b0 <= 246) { val = b0 - 139; vs = 1; }
        if (247 <= b0 && b0 <= 250) { val = (b0 - 247) * 256 + b1 + 108; vs = 2; }
        if (251 <= b0 && b0 <= 254) { val = -(b0 - 251) * 256 - b1 - 108; vs = 2; }
        if (b0 == 255) { val = bin.readInt(data, offset + 1) / 0xffff; vs = 5; throw "unknown number"; }

        if (b0 == 30) {
          var nibs = [];
          vs = 1;
          while (true) {
            var b = data[offset + vs]; vs++;
            var nib0 = b >> 4, nib1 = b & 0xf;
            if (nib0 != 0xf) nibs.push(nib0); if (nib1 != 0xf) nibs.push(nib1);
            if (nib1 == 0xf) break;
          }
          var s = "";
          var chars = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ".", "e", "e-", "reserved", "-", "endOfNumber"];
          for (var i = 0; i < nibs.length; i++) s += chars[nibs[i]];
          //console.log(nibs);
          val = parseFloat(s);
        }

        if (b0 <= 21)	// operator
        {
          var keys = ["version", "Notice", "FullName", "FamilyName", "Weight", "FontBBox", "BlueValues", "OtherBlues", "FamilyBlues", "FamilyOtherBlues",
            "StdHW", "StdVW", "escape", "UniqueID", "XUID", "charset", "Encoding", "CharStrings", "Private", "Subrs",
            "defaultWidthX", "nominalWidthX"];

          key = keys[b0]; vs = 1;
          if (b0 == 12) {
            var keys = ["Copyright", "isFixedPitch", "ItalicAngle", "UnderlinePosition", "UnderlineThickness", "PaintType", "CharstringType", "FontMatrix", "StrokeWidth", "BlueScale",
              "BlueShift", "BlueFuzz", "StemSnapH", "StemSnapV", "ForceBold", "", "", "LanguageGroup", "ExpansionFactor", "initialRandomSeed",
              "SyntheticBase", "PostScript", "BaseFontName", "BaseFontBlend", "", "", "", "", "", "",
              "ROS", "CIDFontVersion", "CIDFontRevision", "CIDFontType", "CIDCount", "UIDBase", "FDArray", "FDSelect", "FontName"];
            key = keys[b1]; vs = 2;
          }
        }

        if (key != null) { dict[key] = carr.length == 1 ? carr[0] : carr; carr = []; }
        else carr.push(val);

        offset += vs;
      }
      return dict;
    }
  };


  Typr["T"].cmap = {
    parseTab: function (data, offset, length) {
      var obj = { tables: [], ids: {}, off: offset };
      data = new Uint8Array(data.buffer, offset, length);
      offset = 0;
      var bin = Typr["B"], rU = bin.readUshort, cmap = Typr["T"].cmap;
      rU(data, offset); offset += 2;
      var numTables = rU(data, offset); offset += 2;

      //console.log(version, numTables);

      var offs = [];


      for (var i = 0; i < numTables; i++) {
        var platformID = rU(data, offset); offset += 2;
        var encodingID = rU(data, offset); offset += 2;
        var noffset = bin.readUint(data, offset); offset += 4;

        var id = "p" + platformID + "e" + encodingID;

        //console.log("cmap subtable", platformID, encodingID, noffset);


        var tind = offs.indexOf(noffset);

        if (tind == -1) {
          tind = obj.tables.length;
          var subt = {};
          offs.push(noffset);
          //var time = Date.now();
          var format = subt.format = rU(data, noffset);
          if (format == 0) subt = cmap.parse0(data, noffset, subt);
          //else if(format== 2) subt.off = noffset;
          else if (format == 4) subt = cmap.parse4(data, noffset, subt);
          else if (format == 6) subt = cmap.parse6(data, noffset, subt);
          else if (format == 12) subt = cmap.parse12(data, noffset, subt);
          //console.log(format, Date.now()-time);
          //else console.log("unknown format: "+format, platformID, encodingID, noffset);
          obj.tables.push(subt);
        }

        if (obj.ids[id] != null) console.log("multiple tables for one platform+encoding: " + id);
        obj.ids[id] = tind;
      }
      return obj;
    },

    parse0: function (data, offset, obj) {
      var bin = Typr["B"];
      offset += 2;
      var len = bin.readUshort(data, offset); offset += 2;
      bin.readUshort(data, offset); offset += 2;
      obj.map = [];
      for (var i = 0; i < len - 6; i++) obj.map.push(data[offset + i]);
      return obj;
    },

    parse4: function (data, offset, obj) {
      var bin = Typr["B"], rU = bin.readUshort, rUs = bin.readUshorts;
      var offset0 = offset;
      offset += 2;
      var length = rU(data, offset); offset += 2;
      rU(data, offset); offset += 2;
      var segCountX2 = rU(data, offset); offset += 2;
      var segCount = segCountX2 >>> 1;
      obj.searchRange = rU(data, offset); offset += 2;
      obj.entrySelector = rU(data, offset); offset += 2;
      obj.rangeShift = rU(data, offset); offset += 2;
      obj.endCount = rUs(data, offset, segCount); offset += segCount * 2;
      offset += 2;
      obj.startCount = rUs(data, offset, segCount); offset += segCount * 2;
      obj.idDelta = [];
      for (var i = 0; i < segCount; i++) { obj.idDelta.push(bin.readShort(data, offset)); offset += 2; }
      obj.idRangeOffset = rUs(data, offset, segCount); offset += segCount * 2;
      obj.glyphIdArray = rUs(data, offset, ((offset0 + length) - offset) >> 1);  //offset += segCount*2;
      return obj;
    },

    parse6: function (data, offset, obj) {
      var bin = Typr["B"];
      offset += 2;
      bin.readUshort(data, offset); offset += 2;
      bin.readUshort(data, offset); offset += 2;
      obj.firstCode = bin.readUshort(data, offset); offset += 2;
      var entryCount = bin.readUshort(data, offset); offset += 2;
      obj.glyphIdArray = [];
      for (var i = 0; i < entryCount; i++) { obj.glyphIdArray.push(bin.readUshort(data, offset)); offset += 2; }

      return obj;
    },

    parse12: function (data, offset, obj) {
      var bin = Typr["B"], rU = bin.readUint;
      offset += 4;
      rU(data, offset); offset += 4;
      rU(data, offset); offset += 4;
      var nGroups = rU(data, offset) * 3; offset += 4;

      var gps = obj.groups = new Uint32Array(nGroups);//new Uint32Array(data.slice(offset, offset+nGroups*12).buffer);

      for (var i = 0; i < nGroups; i += 3) {
        gps[i] = rU(data, offset + (i << 2));
        gps[i + 1] = rU(data, offset + (i << 2) + 4);
        gps[i + 2] = rU(data, offset + (i << 2) + 8);
      }
      return obj;
    }
  };

  Typr["T"].CBLC = {
    parseTab: function (data, offset, length) {
      var bin = Typr["B"], ooff = offset;

      bin.readUshort(data, offset); offset += 2;
      bin.readUshort(data, offset); offset += 2;

      var numSizes = bin.readUint(data, offset); offset += 4;

      var out = [];
      for (var i = 0; i < numSizes; i++) {
        var off = bin.readUint(data, offset); offset += 4;  // indexSubTableArrayOffset
        bin.readUint(data, offset); offset += 4;  // indexTablesSize
        bin.readUint(data, offset); offset += 4;  // numberOfIndexSubTables
        offset += 4;

        offset += 2 * 12;

        bin.readUshort(data, offset); offset += 2;
        bin.readUshort(data, offset); offset += 2;

        //console.log(off,siz,num, sGlyph, eGlyph);

        offset += 4;

        var coff = ooff + off;
        for (var j = 0; j < 3; j++) {
          var fgI = bin.readUshort(data, coff); coff += 2;
          var lgI = bin.readUshort(data, coff); coff += 2;
          var nxt = bin.readUint(data, coff); coff += 4;
          var gcnt = lgI - fgI + 1;
          //console.log(fgI, lgI, nxt);   //if(nxt==0) break;

          var ioff = ooff + off + nxt;

          var inF = bin.readUshort(data, ioff); ioff += 2; if (inF != 1) throw inF;
          var imF = bin.readUshort(data, ioff); ioff += 2;
          var imgo = bin.readUint(data, ioff); ioff += 4;

          var oarr = [];
          for (var gi = 0; gi < gcnt; gi++) {
            var sbitO = bin.readUint(data, ioff + gi * 4); oarr.push(imgo + sbitO);
            //console.log("--",sbitO);
          }
          out.push([fgI, lgI, imF, oarr]);
        }
      }
      return out;
    }
  };

  Typr["T"].CBDT = {
    parseTab: function (data, offset, length) {

      //var maj = bin.readUshort(data,offset);  offset+=2;
      //var min = bin.readUshort(data,offset);  offset+=2;

      return new Uint8Array(data.buffer, data.byteOffset + offset, length);
    }
  };

  Typr["T"].glyf = {
    parseTab: function (data, offset, length, font) {
      var obj = [], ng = font["maxp"]["numGlyphs"];
      for (var g = 0; g < ng; g++) obj.push(null);
      return obj;
    },

    _parseGlyf: function (font, g) {
      var bin = Typr["B"];
      var data = font["_data"], loca = font["loca"];

      if (loca[g] == loca[g + 1]) return null;

      var offset = Typr["findTable"](data, "glyf", font["_offset"])[0] + loca[g];

      var gl = {};

      gl.noc = bin.readShort(data, offset); offset += 2;		// number of contours
      gl.xMin = bin.readShort(data, offset); offset += 2;
      gl.yMin = bin.readShort(data, offset); offset += 2;
      gl.xMax = bin.readShort(data, offset); offset += 2;
      gl.yMax = bin.readShort(data, offset); offset += 2;

      if (gl.xMin >= gl.xMax || gl.yMin >= gl.yMax) return null;

      if (gl.noc > 0) {
        gl.endPts = [];
        for (var i = 0; i < gl.noc; i++) { gl.endPts.push(bin.readUshort(data, offset)); offset += 2; }

        var instructionLength = bin.readUshort(data, offset); offset += 2;
        if ((data.length - offset) < instructionLength) return null;
        gl.instructions = bin.readBytes(data, offset, instructionLength); offset += instructionLength;

        var crdnum = gl.endPts[gl.noc - 1] + 1;
        gl.flags = [];
        for (var i = 0; i < crdnum; i++) {
          var flag = data[offset]; offset++;
          gl.flags.push(flag);
          if ((flag & 8) != 0) {
            var rep = data[offset]; offset++;
            for (var j = 0; j < rep; j++) { gl.flags.push(flag); i++; }
          }
        }
        gl.xs = [];
        for (var i = 0; i < crdnum; i++) {
          var i8 = ((gl.flags[i] & 2) != 0), same = ((gl.flags[i] & 16) != 0);
          if (i8) { gl.xs.push(same ? data[offset] : -data[offset]); offset++; }
          else {
            if (same) gl.xs.push(0);
            else { gl.xs.push(bin.readShort(data, offset)); offset += 2; }
          }
        }
        gl.ys = [];
        for (var i = 0; i < crdnum; i++) {
          var i8 = ((gl.flags[i] & 4) != 0), same = ((gl.flags[i] & 32) != 0);
          if (i8) { gl.ys.push(same ? data[offset] : -data[offset]); offset++; }
          else {
            if (same) gl.ys.push(0);
            else { gl.ys.push(bin.readShort(data, offset)); offset += 2; }
          }
        }
        var x = 0, y = 0;
        for (var i = 0; i < crdnum; i++) { x += gl.xs[i]; y += gl.ys[i]; gl.xs[i] = x; gl.ys[i] = y; }
        //console.log(endPtsOfContours, instructionLength, instructions, flags, xCoordinates, yCoordinates);
      }
      else {
        var ARG_1_AND_2_ARE_WORDS = 1 << 0;
        var ARGS_ARE_XY_VALUES = 1 << 1;
        var WE_HAVE_A_SCALE = 1 << 3;
        var MORE_COMPONENTS = 1 << 5;
        var WE_HAVE_AN_X_AND_Y_SCALE = 1 << 6;
        var WE_HAVE_A_TWO_BY_TWO = 1 << 7;
        var WE_HAVE_INSTRUCTIONS = 1 << 8;

        gl.parts = [];
        var flags;
        do {
          flags = bin.readUshort(data, offset); offset += 2;
          var part = { m: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 }, p1: -1, p2: -1 }; gl.parts.push(part);
          part.glyphIndex = bin.readUshort(data, offset); offset += 2;
          if (flags & ARG_1_AND_2_ARE_WORDS) {
            var arg1 = bin.readShort(data, offset); offset += 2;
            var arg2 = bin.readShort(data, offset); offset += 2;
          } else {
            var arg1 = bin.readInt8(data, offset); offset++;
            var arg2 = bin.readInt8(data, offset); offset++;
          }

          if (flags & ARGS_ARE_XY_VALUES) { part.m.tx = arg1; part.m.ty = arg2; }
          else { part.p1 = arg1; part.p2 = arg2; }
          //part.m.tx = arg1;  part.m.ty = arg2;
          //else { throw "params are not XY values"; }

          if (flags & WE_HAVE_A_SCALE) {
            part.m.a = part.m.d = bin.readF2dot14(data, offset); offset += 2;
          } else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) {
            part.m.a = bin.readF2dot14(data, offset); offset += 2;
            part.m.d = bin.readF2dot14(data, offset); offset += 2;
          } else if (flags & WE_HAVE_A_TWO_BY_TWO) {
            part.m.a = bin.readF2dot14(data, offset); offset += 2;
            part.m.b = bin.readF2dot14(data, offset); offset += 2;
            part.m.c = bin.readF2dot14(data, offset); offset += 2;
            part.m.d = bin.readF2dot14(data, offset); offset += 2;
          }
        } while (flags & MORE_COMPONENTS)
        if (flags & WE_HAVE_INSTRUCTIONS) {
          var numInstr = bin.readUshort(data, offset); offset += 2;
          gl.instr = [];
          for (var i = 0; i < numInstr; i++) { gl.instr.push(data[offset]); offset++; }
        }
      }
      return gl;
    }
  };

  Typr["T"].head = {
    parseTab: function (data, offset, length) {
      var bin = Typr["B"];
      var obj = {};
      bin.readFixed(data, offset); offset += 4;

      obj["fontRevision"] = bin.readFixed(data, offset); offset += 4;
      bin.readUint(data, offset); offset += 4;
      bin.readUint(data, offset); offset += 4;
      obj["flags"] = bin.readUshort(data, offset); offset += 2;
      obj["unitsPerEm"] = bin.readUshort(data, offset); offset += 2;
      obj["created"] = bin.readUint64(data, offset); offset += 8;
      obj["modified"] = bin.readUint64(data, offset); offset += 8;
      obj["xMin"] = bin.readShort(data, offset); offset += 2;
      obj["yMin"] = bin.readShort(data, offset); offset += 2;
      obj["xMax"] = bin.readShort(data, offset); offset += 2;
      obj["yMax"] = bin.readShort(data, offset); offset += 2;
      obj["macStyle"] = bin.readUshort(data, offset); offset += 2;
      obj["lowestRecPPEM"] = bin.readUshort(data, offset); offset += 2;
      obj["fontDirectionHint"] = bin.readShort(data, offset); offset += 2;
      obj["indexToLocFormat"] = bin.readShort(data, offset); offset += 2;
      obj["glyphDataFormat"] = bin.readShort(data, offset); offset += 2;
      return obj;
    }
  };

  Typr["T"].hhea = {
    parseTab: function (data, offset, length) {
      var bin = Typr["B"];
      var obj = {};
      bin.readFixed(data, offset); offset += 4;

      var keys = ["ascender", "descender", "lineGap",
        "advanceWidthMax", "minLeftSideBearing", "minRightSideBearing", "xMaxExtent",
        "caretSlopeRise", "caretSlopeRun", "caretOffset",
        "res0", "res1", "res2", "res3",
        "metricDataFormat", "numberOfHMetrics"];

      for (var i = 0; i < keys.length; i++) {
        var key = keys[i];
        var func = (key == "advanceWidthMax" || key == "numberOfHMetrics") ? bin.readUshort : bin.readShort;
        obj[key] = func(data, offset + i * 2);
      }
      return obj;
    }
  };


  Typr["T"].hmtx = {
    parseTab: function (data, offset, length, font) {
      var bin = Typr["B"];
      var aWidth = [];
      var lsBearing = [];

      var nG = font["maxp"]["numGlyphs"], nH = font["hhea"]["numberOfHMetrics"];
      var aw = 0, lsb = 0, i = 0;
      while (i < nH) { aw = bin.readUshort(data, offset + (i << 2)); lsb = bin.readShort(data, offset + (i << 2) + 2); aWidth.push(aw); lsBearing.push(lsb); i++; }
      while (i < nG) { aWidth.push(aw); lsBearing.push(lsb); i++; }

      return { aWidth: aWidth, lsBearing: lsBearing };
    }
  };


  Typr["T"].kern = {
    parseTab: function (data, offset, length, font) {
      var bin = Typr["B"], kern = Typr["T"].kern;

      var version = bin.readUshort(data, offset);
      if (version == 1) return kern.parseV1(data, offset, length, font);
      var nTables = bin.readUshort(data, offset + 2); offset += 4;

      var map = { glyph1: [], rval: [] };
      for (var i = 0; i < nTables; i++) {
        offset += 2;	// skip version
        var length = bin.readUshort(data, offset); offset += 2;
        var coverage = bin.readUshort(data, offset); offset += 2;
        var format = coverage >>> 8;
  			/* I have seen format 128 once, that's why I do */ format &= 0xf;
        if (format == 0) offset = kern.readFormat0(data, offset, map);
        //else throw "unknown kern table format: "+format;
      }
      return map;
    },

    parseV1: function (data, offset, length, font) {
      var bin = Typr["B"], kern = Typr["T"].kern;

      bin.readFixed(data, offset);   // 0x00010000
      var nTables = bin.readUint(data, offset + 4); offset += 8;

      var map = { glyph1: [], rval: [] };
      for (var i = 0; i < nTables; i++) {
        bin.readUint(data, offset); offset += 4;
        var coverage = bin.readUshort(data, offset); offset += 2;
        bin.readUshort(data, offset); offset += 2;
        var format = coverage & 0xff;
        if (format == 0) offset = kern.readFormat0(data, offset, map);
        //else throw "unknown kern table format: "+format;
      }
      return map;
    },

    readFormat0: function (data, offset, map) {
      var bin = Typr["B"], rUs = bin.readUshort;
      var pleft = -1;
      var nPairs = rUs(data, offset);
      rUs(data, offset + 2);
      rUs(data, offset + 4);
      rUs(data, offset + 6); offset += 8;
      for (var j = 0; j < nPairs; j++) {
        var left = rUs(data, offset); offset += 2;
        var right = rUs(data, offset); offset += 2;
        var value = bin.readShort(data, offset); offset += 2;
        if (left != pleft) { map.glyph1.push(left); map.rval.push({ glyph2: [], vals: [] }); }
        var rval = map.rval[map.rval.length - 1];
        rval.glyph2.push(right); rval.vals.push(value);
        pleft = left;
      }
      return offset;
    }
  };


  Typr["T"].loca = {
    parseTab: function (data, offset, length, font) {
      var bin = Typr["B"];
      var obj = [];

      var ver = font["head"]["indexToLocFormat"];
      var len = font["maxp"]["numGlyphs"] + 1;

      if (ver == 0) for (var i = 0; i < len; i++) obj.push(bin.readUshort(data, offset + (i << 1)) << 1);
      if (ver == 1) for (var i = 0; i < len; i++) obj.push(bin.readUint(data, offset + (i << 2)));

      return obj;
    }
  };


  Typr["T"].maxp = {
    parseTab: function (data, offset, length) {
      //console.log(data.length, offset, length);

      var bin = Typr["B"], rU = bin.readUshort;
      var obj = {};

      // both versions 0.5 and 1.0
      bin.readUint(data, offset); offset += 4;

      obj["numGlyphs"] = rU(data, offset); offset += 2;

      // only 1.0
      /*
      if(ver == 0x00010000) {
        obj.maxPoints             = rU(data, offset);  offset += 2;
        obj.maxContours           = rU(data, offset);  offset += 2;
        obj.maxCompositePoints    = rU(data, offset);  offset += 2;
        obj.maxCompositeContours  = rU(data, offset);  offset += 2;
        obj.maxZones              = rU(data, offset);  offset += 2;
        obj.maxTwilightPoints     = rU(data, offset);  offset += 2;
        obj.maxStorage            = rU(data, offset);  offset += 2;
        obj.maxFunctionDefs       = rU(data, offset);  offset += 2;
        obj.maxInstructionDefs    = rU(data, offset);  offset += 2;
        obj.maxStackElements      = rU(data, offset);  offset += 2;
        obj.maxSizeOfInstructions = rU(data, offset);  offset += 2;
        obj.maxComponentElements  = rU(data, offset);  offset += 2;
        obj.maxComponentDepth     = rU(data, offset);  offset += 2;
      }
      */

      return obj;
    }
  };
  Typr["T"].name = {
    parseTab: function (data, offset, length) {
      var bin = Typr["B"];
      var obj = {};
      bin.readUshort(data, offset); offset += 2;
      var count = bin.readUshort(data, offset); offset += 2;
      var stringOffset = bin.readUshort(data, offset); offset += 2;

      var ooo = offset - 6 + stringOffset;
      //console.log(format,count);

      var names = [
        "copyright",
        "fontFamily",
        "fontSubfamily",
        "ID",
        "fullName",
        "version",
        "postScriptName",
        "trademark",
        "manufacturer",
        "designer",
        "description",
        "urlVendor",
        "urlDesigner",
        "licence",
        "licenceURL",
        "---",
        "typoFamilyName",
        "typoSubfamilyName",
        "compatibleFull",
        "sampleText",
        "postScriptCID",
        "wwsFamilyName",
        "wwsSubfamilyName",
        "lightPalette",
        "darkPalette"
      ];

      var rU = bin.readUshort;

      for (var i = 0; i < count; i++) {
        var platformID = rU(data, offset); offset += 2;
        var encodingID = rU(data, offset); offset += 2;
        var languageID = rU(data, offset); offset += 2;
        var nameID = rU(data, offset); offset += 2;
        var slen = rU(data, offset); offset += 2;
        var noffset = rU(data, offset); offset += 2;
        //console.log(platformID, encodingID, languageID.toString(16), nameID, length, noffset);


        var soff = ooo + noffset;
        var str;
        if (platformID == 0) str = bin.readUnicode(data, soff, slen / 2);
        else if (platformID == 3 && encodingID == 0) str = bin.readUnicode(data, soff, slen / 2);
        else if (platformID == 1 && encodingID == 25) str = bin.readUnicode(data, soff, slen / 2);
        else if (encodingID == 0) str = bin.readASCII(data, soff, slen);
        else if (encodingID == 1) str = bin.readUnicode(data, soff, slen / 2);
        else if (encodingID == 3) str = bin.readUnicode(data, soff, slen / 2);
        else if (encodingID == 4) str = bin.readUnicode(data, soff, slen / 2);
        else if (encodingID == 5) str = bin.readUnicode(data, soff, slen / 2);
        else if (encodingID == 10) str = bin.readUnicode(data, soff, slen / 2);

        else if (platformID == 1) { str = bin.readASCII(data, soff, slen); console.log("reading unknown MAC encoding " + encodingID + " as ASCII"); }
        else {
          console.log("unknown encoding " + encodingID + ", platformID: " + platformID);
          str = bin.readASCII(data, soff, slen);
        }

        var tid = "p" + platformID + "," + (languageID).toString(16);//Typr._platforms[platformID];
        if (obj[tid] == null) obj[tid] = {};
        var name = names[nameID]; if (name == null) name = "_" + nameID;
        obj[tid][name] = str;
        obj[tid]["_lang"] = languageID;
        //console.log(tid, obj[tid]);
      }
      /*
      if(format == 1)
      {
        var langTagCount = bin.readUshort(data, offset);  offset += 2;
        for(var i=0; i<langTagCount; i++)
        {
          var length  = bin.readUshort(data, offset);  offset += 2;
          var noffset = bin.readUshort(data, offset);  offset += 2;
        }
      }
      */
      var out = Typr["T"].name.selectOne(obj), ff = "fontFamily";
      if (out[ff] == null) for (var p in obj) if (obj[p][ff] != null) out[ff] = obj[p][ff];
      return out;
    },
    selectOne: function (obj) {
      //console.log(obj);
      var psn = "postScriptName";

      for (var p in obj) if (obj[p][psn] != null && obj[p]["_lang"] == 0x0409) return obj[p];		// United States
      for (var p in obj) if (obj[p][psn] != null && obj[p]["_lang"] == 0x0000) return obj[p];		// Universal
      for (var p in obj) if (obj[p][psn] != null && obj[p]["_lang"] == 0x0c0c) return obj[p];		// Canada
      for (var p in obj) if (obj[p][psn] != null) return obj[p];

      var out;
      for (var p in obj) { out = obj[p]; break; }
      console.log("returning name table with languageID " + out._lang);
      if (out[psn] == null && out["ID"] != null) out[psn] = out["ID"];
      return out;
    }
  };

  Typr["T"].OS2 = {
    parseTab: function (data, offset, length) {
      var bin = Typr["B"];
      var ver = bin.readUshort(data, offset); offset += 2;

      var OS2 = Typr["T"].OS2;

      var obj = {};
      if (ver == 0) OS2.version0(data, offset, obj);
      else if (ver == 1) OS2.version1(data, offset, obj);
      else if (ver == 2 || ver == 3 || ver == 4) OS2.version2(data, offset, obj);
      else if (ver == 5) OS2.version5(data, offset, obj);
      else throw "unknown OS/2 table version: " + ver;

      return obj;
    },

    version0: function (data, offset, obj) {
      var bin = Typr["B"];
      obj["xAvgCharWidth"] = bin.readShort(data, offset); offset += 2;
      obj["usWeightClass"] = bin.readUshort(data, offset); offset += 2;
      obj["usWidthClass"] = bin.readUshort(data, offset); offset += 2;
      obj["fsType"] = bin.readUshort(data, offset); offset += 2;
      obj["ySubscriptXSize"] = bin.readShort(data, offset); offset += 2;
      obj["ySubscriptYSize"] = bin.readShort(data, offset); offset += 2;
      obj["ySubscriptXOffset"] = bin.readShort(data, offset); offset += 2;
      obj["ySubscriptYOffset"] = bin.readShort(data, offset); offset += 2;
      obj["ySuperscriptXSize"] = bin.readShort(data, offset); offset += 2;
      obj["ySuperscriptYSize"] = bin.readShort(data, offset); offset += 2;
      obj["ySuperscriptXOffset"] = bin.readShort(data, offset); offset += 2;
      obj["ySuperscriptYOffset"] = bin.readShort(data, offset); offset += 2;
      obj["yStrikeoutSize"] = bin.readShort(data, offset); offset += 2;
      obj["yStrikeoutPosition"] = bin.readShort(data, offset); offset += 2;
      obj["sFamilyClass"] = bin.readShort(data, offset); offset += 2;
      obj["panose"] = bin.readBytes(data, offset, 10); offset += 10;
      obj["ulUnicodeRange1"] = bin.readUint(data, offset); offset += 4;
      obj["ulUnicodeRange2"] = bin.readUint(data, offset); offset += 4;
      obj["ulUnicodeRange3"] = bin.readUint(data, offset); offset += 4;
      obj["ulUnicodeRange4"] = bin.readUint(data, offset); offset += 4;
      obj["achVendID"] = bin.readASCII(data, offset, 4); offset += 4;
      obj["fsSelection"] = bin.readUshort(data, offset); offset += 2;
      obj["usFirstCharIndex"] = bin.readUshort(data, offset); offset += 2;
      obj["usLastCharIndex"] = bin.readUshort(data, offset); offset += 2;
      obj["sTypoAscender"] = bin.readShort(data, offset); offset += 2;
      obj["sTypoDescender"] = bin.readShort(data, offset); offset += 2;
      obj["sTypoLineGap"] = bin.readShort(data, offset); offset += 2;
      obj["usWinAscent"] = bin.readUshort(data, offset); offset += 2;
      obj["usWinDescent"] = bin.readUshort(data, offset); offset += 2;
      return offset;
    },

    version1: function (data, offset, obj) {
      var bin = Typr["B"];
      offset = Typr["T"].OS2.version0(data, offset, obj);

      obj["ulCodePageRange1"] = bin.readUint(data, offset); offset += 4;
      obj["ulCodePageRange2"] = bin.readUint(data, offset); offset += 4;
      return offset;
    },

    version2: function (data, offset, obj) {
      var bin = Typr["B"], rU = bin.readUshort;
      offset = Typr["T"].OS2.version1(data, offset, obj);

      obj["sxHeight"] = bin.readShort(data, offset); offset += 2;
      obj["sCapHeight"] = bin.readShort(data, offset); offset += 2;
      obj["usDefault"] = rU(data, offset); offset += 2;
      obj["usBreak"] = rU(data, offset); offset += 2;
      obj["usMaxContext"] = rU(data, offset); offset += 2;
      return offset;
    },

    version5: function (data, offset, obj) {
      var rU = Typr["B"].readUshort;
      offset = Typr["T"].OS2.version2(data, offset, obj);

      obj["usLowerOpticalPointSize"] = rU(data, offset); offset += 2;
      obj["usUpperOpticalPointSize"] = rU(data, offset); offset += 2;
      return offset;
    }
  };

  Typr["T"].post = {
    parseTab: function (data, offset, length) {
      var bin = Typr["B"];
      var obj = {};

      obj["version"] = bin.readFixed(data, offset); offset += 4;
      obj["italicAngle"] = bin.readFixed(data, offset); offset += 4;
      obj["underlinePosition"] = bin.readShort(data, offset); offset += 2;
      obj["underlineThickness"] = bin.readShort(data, offset); offset += 2;

      return obj;
    }
  };
  Typr["T"].SVG = {
    parseTab: function (data, offset, length) {
      var bin = Typr["B"];
      var obj = { entries: [], svgs: [] };

      var offset0 = offset;

      bin.readUshort(data, offset); offset += 2;
      var svgDocIndexOffset = bin.readUint(data, offset); offset += 4;
      bin.readUint(data, offset); offset += 4;

      offset = svgDocIndexOffset + offset0;

      var numEntries = bin.readUshort(data, offset); offset += 2;

      for (var i = 0; i < numEntries; i++) {
        var startGlyphID = bin.readUshort(data, offset); offset += 2;
        var endGlyphID = bin.readUshort(data, offset); offset += 2;
        var svgDocOffset = bin.readUint(data, offset); offset += 4;
        var svgDocLength = bin.readUint(data, offset); offset += 4;

        var sbuf = new Uint8Array(data.buffer, offset0 + svgDocOffset + svgDocIndexOffset, svgDocLength);
        if (sbuf[0] == 0x1f && sbuf[1] == 0x8b && sbuf[2] == 0x08) sbuf = pako["inflate"](sbuf);
        var svg = bin.readUTF8(sbuf, 0, sbuf.length);

        for (var f = startGlyphID; f <= endGlyphID; f++) {
          obj.entries[f] = obj.svgs.length;
        }
        obj.svgs.push(svg);
      }
      return obj;
    }
  };


  Typr["T"].sbix = {
    parseTab: function (data, offset, length, obj) {
      var numGlyphs = obj["maxp"]["numGlyphs"];
      var ooff = offset;
      var bin = Typr["B"];

      //var ver = bin.readUshort(data,offset);  offset+=2;
      //var flg = bin.readUshort(data,offset);  offset+=2;

      var numStrikes = bin.readUint(data, offset + 4);

      var out = [];
      for (var si = numStrikes - 1; si < numStrikes; si++) {
        var off = ooff + bin.readUint(data, offset + 8 + si * 4);

        //var ppem = bin.readUshort(data,off);  off+=2;
        //var ppi  = bin.readUshort(data,off);  off+=2;

        for (var gi = 0; gi < numGlyphs; gi++) {
          var aoff = bin.readUint(data, off + 4 + gi * 4);
          var noff = bin.readUint(data, off + 4 + gi * 4 + 4); if (aoff == noff) { out[gi] = null; continue; }
          var go = off + aoff;
          //var ooX = bin.readUshort(data,go);
          //var ooY = bin.readUshort(data,go+2);
          var tag = bin.readASCII(data, go + 4, 4); if (tag != "png ") throw tag;

          out[gi] = new Uint8Array(data.buffer, data.byteOffset + go + 8, noff - aoff - 8);
        }
      }
      return out;
    }
  };

  Typr["T"].colr = {
    parseTab: function (data, offset, length) {
      var bin = Typr["B"];
      var ooff = offset;
      offset += 2;
      var num = bin.readUshort(data, offset); offset += 2;

      var boff = bin.readUint(data, offset); offset += 4;
      var loff = bin.readUint(data, offset); offset += 4;

      var lnum = bin.readUshort(data, offset); offset += 2;
      //console.log(num,boff,loff,lnum);

      var base = {};
      var coff = ooff + boff;
      for (var i = 0; i < num; i++) {
        base["g" + bin.readUshort(data, coff)] = [bin.readUshort(data, coff + 2), bin.readUshort(data, coff + 4)];
        coff += 6;
      }

      var lays = [];
      coff = ooff + loff;
      for (var i = 0; i < lnum; i++) {
        lays.push(bin.readUshort(data, coff), bin.readUshort(data, coff + 2)); coff += 4;
      }
      return [base, lays];
    }
  };

  Typr["T"].cpal = {
    parseTab: function (data, offset, length) {
      var bin = Typr["B"];
      var ooff = offset;
      var vsn = bin.readUshort(data, offset); offset += 2;

      if (vsn == 0) {
        bin.readUshort(data, offset); offset += 2;
        bin.readUshort(data, offset); offset += 2;
        var tot = bin.readUshort(data, offset); offset += 2;

        var fst = bin.readUint(data, offset); offset += 4;

        return new Uint8Array(data.buffer, ooff + fst, tot * 4);
        /*
        var coff=ooff+fst;

        for(var i=0; i<tot; i++) {
          console.log(data[coff],data[coff+1],data[coff+2],data[coff+3]);
          coff+=4;
        }

        console.log(ets,pts,tot); */
      }
      else throw vsn;//console.log("unknown color palette",vsn);
    }
  };

  Typr["T"].GSUB = {
    parseTab: function (data, offset, length, obj) {
      //console.log(obj.name.ID);

      var bin = Typr["B"], rU = bin.readUshort;


      var off = offset;
      rU(data, off); off += 2;
      rU(data, off); off += 2;
      rU(data, off); off += 2;
      var flO = rU(data, off); off += 2;
      rU(data, off); off += 2;

      //console.log(maj,min,slO,flO,llO);

      off = offset + flO;

      var fmap = {};
      var cnt = rU(data, off); off += 2;
      for (var i = 0; i < cnt; i++) {
        var tag = bin.readASCII(data, off, 4); off += 4;
        rU(data, off); off += 2;
        fmap[tag] = true;
      }
      //console.log(fmap);
      return fmap;
    }
  };

  Typr["T"].fvar = {
    parseTab: function (data, offset, length, obj) {
      var name = obj["name"];
      var off = offset;
      var bin = Typr["B"];
      var axes = [], inst = [];

      off += 8;
      var acnt = bin.readUshort(data, off); off += 2;
      off += 2;
      var icnt = bin.readUshort(data, off); off += 2;
      var isiz = bin.readUshort(data, off); off += 2;

      for (var i = 0; i < acnt; i++) {
        var tag = bin.readASCII(data, off, 4);
        var min = bin.readFixed(data, off + 4);
        var def = bin.readFixed(data, off + 8);
        var max = bin.readFixed(data, off + 12);
        var flg = bin.readUshort(data, off + 16);
        var nid = bin.readUshort(data, off + 18);
        axes.push([tag, min, def, max, flg, name["_" + nid]]);
        //console.log(tag,min,def,max,flg,nid);
        off += 20;
      }
      for (var i = 0; i < icnt; i++) {
        var snid = bin.readUshort(data, off), pnid = null;
        var flg = bin.readUshort(data, off + 2);
        var crd = []; for (var j = 0; j < acnt; j++) crd.push(bin.readFixed(data, off + 4 + j * 4));
        off += 4 + acnt * 4;
        if ((isiz & 3) == 2) { pnid = bin.readUshort(data, off); off += 2; }
        inst.push([name["_" + snid], flg, crd, pnid]);
        //console.log(snid,flg, crd);
      }

      return [axes, inst];
    }
  };

  Typr["T"].gvar = (function () {

    var EMBEDDED_PEAK_TUPLE = 0x8000;
    var INTERMEDIATE_REGION = 0x4000;
    var PRIVATE_POINT_NUMBERS = 0x2000;

    var DELTAS_ARE_ZERO = 0x80;
    var DELTAS_ARE_WORDS = 0x40;

    var POINTS_ARE_WORDS = 0x80;

    var SHARED_POINT_NUMBERS = 0x8000;

    var bin = Typr["B"];

    function readTuple(data, o, acnt) {
      var tup = []; for (var j = 0; j < acnt; j++) tup.push(bin.readF2dot14(data, o + j * 2));
      return tup;
    }

    function readTupleVarHeader(data, off, vcnt, acnt, eoff) {
      var out = [];
      for (var j = 0; j < vcnt; j++) {
        var dsiz = bin.readUshort(data, off); off += 2;
        var tind = bin.readUshort(data, off), flag = tind & 0xf000; tind = tind & 0xfff; off += 2;
        //console.log(j, dsiz,tind, flag.toString(16));

        var peak = null, start = null, end = null;
        if (flag & EMBEDDED_PEAK_TUPLE) { peak = readTuple(data, off, acnt); off += acnt * 2; }
        if (flag & INTERMEDIATE_REGION) { start = readTuple(data, off, acnt); off += acnt * 2; }
        if (flag & INTERMEDIATE_REGION) { end = readTuple(data, off, acnt); off += acnt * 2; }
        out.push([dsiz, tind, flag, start, peak, end]);
      }
      return out;
    }

    // Packed "point" numbers
    function readPointNumbers(data, off, gid) {
      var cnt = data[off]; off++; if (cnt == 0) return [[], off];
      if (127 < cnt) { cnt = ((cnt & 127) << 8) | data[off++]; }

      //if(gid==116) console.log("---",cnt);
      var pts = [], last = 0;  // point number data runs
      while (pts.length < cnt) {
        var v = data[off]; off++;
        var wds = (v & POINTS_ARE_WORDS) != 0; v = (v & 127) + 1;
        //if(gid==116) console.log("-",v);
        for (var i = 0; i < v; i++) {
          var dif = 0;
          if (wds) { dif = bin.readUshort(data, off); off += 2; }
          else { dif = data[off]; off++; }
          //if(gid==116) console.log(dif);
          last += dif;
          pts.push(last);
        }
      }
      //console.log(pts);
      return [pts, off];


      //throw "e";
    }


    function parseTab(data, offset, length, obj) {
      var off = offset + 4;
      var acnt = bin.readUshort(data, off); off += 2;
      var tcnt = bin.readUshort(data, off); off += 2;
      var toff = bin.readUint(data, off); off += 4;
      var gcnt = bin.readUshort(data, off); off += 2;
      bin.readUshort(data, off); off += 2;

      var goff = bin.readUint(data, off); off += 4;

      // glyphVariationDataOffsets
      var offs = []; for (var i = 0; i < gcnt + 1; i++) offs.push(bin.readUint(data, off + i * 4));


      // sharedTuples
      var tups = [], mins = [], maxs = []; off = offset + toff;
      for (var i = 0; i < tcnt; i++) {
        var peak = readTuple(data, off + i * acnt * 2, acnt), imin = [], imax = []; tups.push(peak); mins.push(imin); maxs.push(imax);
        for (var k = 0; k < acnt; k++) {
          imin[k] = Math.min(peak[k], 0);
          imax[k] = Math.max(peak[k], 0);
        }
      }
      //console.log(tups);

      //console.log(acnt,stcnt,stoff,gcnt,flgs,goff);

      var i8 = new Int8Array(data.buffer);

      // GlyphVariationData table array
      var tabs = [];
      for (var i = 0; i < gcnt; i++) {
        //console.log("-------",i);
        off = offset + goff + offs[i];
        // tupleVariationCount
        var vcnt = bin.readUshort(data, off); off += 2;  //if((vcnt>>>12)!=0) throw "e";

        var snum = vcnt & SHARED_POINT_NUMBERS; vcnt &= 0xfff;
        //  offset to the serialized data
        var soff = bin.readUshort(data, off); off += 2;

        var hdr = readTupleVarHeader(data, off, vcnt, acnt);

        var tab = []; tabs.push(tab);
        // Serialized Data
        off = offset + goff + offs[i] + soff;

        var sind = null;
        if (snum) {
          var oo = readPointNumbers(data, off);
          sind = oo[0]; off = oo[1];
        }

        for (var j = 0; j < vcnt; j++) {
          var vr = hdr[j], end = off + vr[0];  //console.log(vr);  console.log(data.slice(off,off+vr[0]));

          var ind = sind;
          if (vr[2] & PRIVATE_POINT_NUMBERS) {
            var oo = readPointNumbers(data, off);
            ind = oo[0]; off = oo[1];
          }
          // read packed deltas (delta runs)
          var ds = [];
          while (off < end) {
            var cb = data[off++];  // control byte;
            var cnt = (cb & 0x3f) + 1;
            if (cb & DELTAS_ARE_ZERO) { for (var k = 0; k < cnt; k++) ds.push(0); }
            else if (cb & DELTAS_ARE_WORDS) { for (var k = 0; k < cnt; k++) ds.push(bin.readShort(data, off + k * 2)); off += cnt * 2; }
            else { for (var k = 0; k < cnt; k++) ds.push(i8[off + k]); off += cnt; }
          }
          //if(ind) console.log(ind, ds);
          var ti = vr[1];

          tab.push([[
            vr[3] ? vr[3] : mins[ti],
            vr[4] ? vr[4] : tups[ti],
            vr[5] ? vr[5] : maxs[ti]
          ], ds, ind.length == 0 ? null : ind]);

          if (ind.length != 0 && ind.length * 2 != ds.length) throw "e";
          //if(i==116) console.log(ind, ds);
        }
      }
      return tabs;
    }

    return { parseTab: parseTab };
  })();

  Typr["T"].avar = {
    parseTab: function (data, offset, length, obj) {
      var off = offset;
      var bin = Typr["B"], out = [];

      off += 6;
      var acnt = bin.readUshort(data, off); off += 2;

      for (var ai = 0; ai < acnt; ai++) {
        var cnt = bin.readUshort(data, off); off += 2;
        var poly = []; out.push(poly);
        for (var i = 0; i < cnt; i++) {
          var x = bin.readF2dot14(data, off);
          var y = bin.readF2dot14(data, off + 2); off += 4;
          poly.push(x, y);
        }
      }

      return out;
    }
  };

  Typr["T"].HVAR = {
    parseTab: function (data, offset, length, obj) {
      var off = offset, oo = offset;
      var bin = Typr["B"];

      //console.log(data.slice(off));
      off += 4;

      var varO = bin.readUint(data, off); off += 4;
      var advO = bin.readUint(data, off); off += 4;
      var lsbO = bin.readUint(data, off); off += 4;
      var rsbO = bin.readUint(data, off); off += 4;
      if (lsbO != 0 || rsbO != 0) throw lsbO;

      //console.log(varO,advO,lsbO,rsbO);

      off = oo + varO;  // item variation store

      // ItemVariationStore
      var ioff = off;

      var fmt = bin.readUshort(data, off); off += 2; if (fmt != 1) throw "e";
      var vregO = bin.readUint(data, off); off += 4;
      // itemVariationDataCount
      var vcnt = bin.readUshort(data, off); off += 2;

      var offs = []; for (var i = 0; i < vcnt; i++) offs.push(bin.readUint(data, off + i * 4)); off += vcnt * 4;  //if(offs.length!=1) throw "e";
      //console.log(vregO,vcnt,offs);

      off = ioff + vregO;
      var acnt = bin.readUshort(data, off); off += 2;
      var rcnt = bin.readUshort(data, off); off += 2;

      var regs = [];
      for (var i = 0; i < rcnt; i++) {
        var crd = [[], [], []]; regs.push(crd);
        for (var j = 0; j < acnt; j++) {
          crd[0].push(bin.readF2dot14(data, off + 0));
          crd[1].push(bin.readF2dot14(data, off + 2));
          crd[2].push(bin.readF2dot14(data, off + 4));
          off += 6;
        }
      }
      //console.log(acnt, rcnt, regs);


      var i8 = new Int8Array(data.buffer);
      var varStore = [];
      for (var i = 0; i < offs.length; i++) {
        // ItemVariationData
        off = oo + varO + offs[i]; var vdata = []; varStore.push(vdata);
        var icnt = bin.readUshort(data, off); off += 2;  // itemCount
        var dcnt = bin.readUshort(data, off); off += 2; if (dcnt & 0x8000) throw "e";
        var rcnt = bin.readUshort(data, off); off += 2;
        var ixs = []; for (var j = 0; j < rcnt; j++) ixs.push(bin.readUshort(data, off + j * 2)); off += rcnt * 2;
        //console.log(icnt,dcnt,rcnt,ixs);
        //console.log(data.slice(off));

        for (var k = 0; k < icnt; k++) {  // deltaSets
          var deltaData = [];  //vdata.push(deltaData);
          for (var ri = 0; ri < rcnt; ri++) {
            deltaData.push(ri < dcnt ? bin.readShort(data, off) : i8[off]);
            off += ri < dcnt ? 2 : 1;

          }
          var dd = new Array(regs.length); dd.fill(0); vdata.push(dd);
          for (var j = 0; j < ixs.length; j++) dd[ixs[j]] = deltaData[j];
        }
      }

      //console.log(varStore);

      // VariationRegionList



      off = oo + advO;  // advance widths

      // DeltaSetIndexMap

      var fmt = data[off++]; if (fmt != 0) throw "e";
      var entryFormat = data[off++];

      var mapCount = bin.readUshort(data, off); off += 2;

      var INNER_INDEX_BIT_COUNT_MASK = 0x0f;
      var MAP_ENTRY_SIZE_MASK = 0x30;
      var entrySize = ((entryFormat & MAP_ENTRY_SIZE_MASK) >> 4) + 1;  //if(entrySize!=1) throw entrySize;

      //console.log(fmt, entryFormat, mapCount, entrySize);

      var dfs = [];
      for (var i = 0; i < mapCount; i++) {
        var entry = 0;
        if (entrySize == 1) entry = data[off++];
        else { entry = bin.readUshort(data, off); off += 2; }
        var outerIndex = entry >> ((entryFormat & INNER_INDEX_BIT_COUNT_MASK) + 1);
        var innerIndex = entry & ((1 << ((entryFormat & INNER_INDEX_BIT_COUNT_MASK) + 1)) - 1);
        //map.push(outerIndex,innerIndex);
        dfs.push(varStore[outerIndex][innerIndex]);
        //console.log(outerIndex,innerIndex);
        //console.log(i,varStore[outerIndex][innerIndex]);
      }

      return [regs, dfs];
    }
  };

  Typr["U"] = function () {
    var P = {
      MoveTo: function (p, x, y) { p.cmds.push("M"); p.crds.push(x, y); },
      LineTo: function (p, x, y) { p.cmds.push("L"); p.crds.push(x, y); },
      CurveTo: function (p, a, b, c, d, e, f) { p.cmds.push("C"); p.crds.push(a, b, c, d, e, f); },
      qCurveTo: function (p, a, b, c, d) { p.cmds.push("Q"); p.crds.push(a, b, c, d); },
      ClosePath: function (p) { p.cmds.push("Z"); }
    };

    function getGlyphPosition(font, gls, i1, ltr) {
      var g1 = gls[i1], g2 = gls[i1 + 1], kern = font["kern"];
      if (kern) {
        var ind1 = kern.glyph1.indexOf(g1);
        if (ind1 != -1) {
          var ind2 = kern.rval[ind1].glyph2.indexOf(g2);
          if (ind2 != -1) return [0, 0, kern.rval[ind1].vals[ind2], 0];
        }
      }
      //console.log("no kern");
      return [0, 0, 0, 0];
    }
    function shape(font, str, prm) {
      if (prm == null) prm = {};
      var axs = prm["axs"];
      if (font["fvar"] && axs == null) axs = font["fvar"][1][font["_index"]][2];

      var HVAR = font["HVAR"];  //console.log(HVAR);
      if (axs && HVAR) { axs = _normalizeAxis(font, axs); }  //console.log(S,axs);
      var gls = [];
      for (var i = 0; i < str.length; i++) {
        var cc = str.codePointAt(i); if (cc > 0xffff) i++;
        gls.push(codeToGlyph(font, cc));
      }
      var shape = [];

      for (var i = 0; i < gls.length; i++) {
        var padj = getGlyphPosition(font, gls, i);
        var gid = gls[i];  //console.log(gid);
        var ax = font["hmtx"].aWidth[gid] + padj[2];
        if (HVAR && HVAR[1][gid]) { //ax+=S*HVAR[1][gid][0];
          var difs = HVAR[1][gid];  //console.log(difs);
          for (var j = 0; j < HVAR[0].length; j++) {
            ax += _interpolate(HVAR[0][j], axs) * difs[j];
          }
        }
        shape.push({ "g": gid, "cl": i, "dx": 0, "dy": 0, "ax": ax, "ay": 0 });
      }
      return shape;
    }

    function shapeToPath(font, shape, prm) {
      var tpath = { cmds: [], crds: [] };
      var x = 0, y = 0, clr, axs;
      if (prm) { clr = prm["clr"]; axs = prm["axs"]; }

      for (var i = 0; i < shape.length; i++) {
        var it = shape[i];
        var path = glyphToPath(font, it["g"], false, axs), crds = path["crds"];
        for (var j = 0; j < crds.length; j += 2) {
          tpath.crds.push(crds[j] + x + it["dx"]);
          tpath.crds.push(crds[j + 1] + y + it["dy"]);
        }
        if (clr) tpath.cmds.push(clr);
        for (var j = 0; j < path["cmds"].length; j++) tpath.cmds.push(path["cmds"][j]);
        var clen = tpath.cmds.length;
        if (clr) if (clen != 0 && tpath.cmds[clen - 1] != "X") tpath.cmds.push("X");  // SVG fonts might contain "X". Then, nothing would stroke non-SVG glyphs.

        x += it["ax"]; y += it["ay"];
      }
      return { "cmds": tpath.cmds, "crds": tpath.crds };
    }


    // find the greatest index with a value <=v
    function arrSearch(arr, k, v) {
      var l = 0, r = ~~(arr.length / k);
      while (l + 1 != r) { var mid = l + ((r - l) >>> 1); if (arr[mid * k] <= v) l = mid; else r = mid; }

      //var mi = 0;  for(var i=0; i<arr.length; i+=k) if(arr[i]<=v) mi=i;  if(mi!=l*k) throw "e";

      return l * k;
    }

    var wha = [0x9, 0xa, 0xb, 0xc, 0xd, 0x20, 0x85, 0xa0, 0x1680, 0x180e, 0x2028, 0x2029, 0x202f, 0x2060, 0x3000, 0xfeff], whm = {};
    for (var i = 0; i < wha.length; i++) whm[wha[i]] = 1;
    for (var i = 0x2000; i <= 0x200d; i++) whm[i] = 1;

    function codeToGlyph(font, code) {
      //console.log(cmap);
      // "p3e10" for NotoEmoji-Regular.ttf
      //console.log(cmap);

      if (font["_ctab"] == null) {
        var cmap = font["cmap"];
        var tind = -1, pps = ["p3e10", "p0e4", "p3e1", "p1e0", "p0e3", "p0e1"/*,"p3e3"*/, "p3e0" /*Hebrew*/, "p3e5" /*Korean*/];
        for (var i = 0; i < pps.length; i++) if (cmap.ids[pps[i]] != null) { tind = cmap.ids[pps[i]]; break; }
        if (tind == -1) throw "no familiar platform and encoding!";
        font["_ctab"] = cmap.tables[tind];
      }

      var tab = font["_ctab"], fmt = tab.format, gid = -1;  //console.log(fmt); throw "e";

      if (fmt == 0) {
        if (code >= tab.map.length) gid = 0;
        else gid = tab.map[code];
      }
      /*else if(fmt==2) {
        var data=font["_data"], off = cmap.off+tab.off+6, bin=Typr["B"];
        var shKey = bin.readUshort(data,off + 2*(code>>>8));
        var shInd = off + 256*2 + shKey*8;

        var firstCode = bin.readUshort(data,shInd);
        var entryCount= bin.readUshort(data,shInd+2);
        var idDelta   = bin.readShort (data,shInd+4);
        var idRangeOffset = bin.readUshort(data,shInd+6);

        if(firstCode<=code && code<=firstCode+entryCount) {
          // not completely correct
          gid = bin.readUshort(data, shInd+6+idRangeOffset + (code&255)*2);
        }
        else gid=0;
        //if(code>256) console.log(code,(code>>>8),shKey,firstCode,entryCount,idDelta,idRangeOffset);

        //throw "e";
        //console.log(tab,  bin.readUshort(data,off));
        //throw "e";
      }*/
      else if (fmt == 4) {
        var ec = tab.endCount; gid = 0;
        if (code <= ec[ec.length - 1]) {
          // smallest index with code <= value
          var sind = arrSearch(ec, 1, code);
          if (ec[sind] < code) sind++;

          if (code >= tab.startCount[sind]) {
            var gli = 0;
            if (tab.idRangeOffset[sind] != 0) gli = tab.glyphIdArray[(code - tab.startCount[sind]) + (tab.idRangeOffset[sind] >> 1) - (tab.idRangeOffset.length - sind)];
            else gli = code + tab.idDelta[sind];
            gid = (gli & 0xFFFF);
          }
        }
      }
      else if (fmt == 6) {
        var off = code - tab.firstCode, arr = tab.glyphIdArray;
        if (off < 0 || off >= arr.length) gid = 0;
        else gid = arr[off];
      }
      else if (fmt == 12) {
        var grp = tab.groups; gid = 0;  //console.log(grp);  throw "e";

        if (code <= grp[grp.length - 2]) {
          var i = arrSearch(grp, 3, code);
          if (grp[i] <= code && code <= grp[i + 1]) { gid = grp[i + 2] + (code - grp[i]); }
        }
      }
      else throw "unknown cmap table format " + tab.format;

      //*
      var SVG = font["SVG "], loca = font["loca"];
      // if the font claims to have a Glyph for a character, but the glyph is empty, and the character is not "white", it is a lie!
      if (gid != 0 && font["CFF "] == null && (SVG == null || SVG.entries[gid] == null) && loca && loca[gid] == loca[gid + 1]  // loca not present in CFF or SVG fonts
        && whm[code] == null) gid = 0;
      //*/

      return gid;
    }
    function glyphToPath(font, gid, noColor, axs) {
      var path = { cmds: [], crds: [] };

      if (font["fvar"]) {
        if (axs == null) axs = font["fvar"][1][font["_index"]][2];
        axs = _normalizeAxis(font, axs);
      }

      var SVG = font["SVG "], CFF = font["CFF "], COLR = font["COLR"], CBLC = font["CBLC"], CBDT = font["CBDT"], sbix = font["sbix"], upng = window["UPNG"];

      var strike = null;
      if (CBLC && upng) for (var i = 0; i < CBLC.length; i++) if (CBLC[i][0] <= gid && gid <= CBLC[i][1]) strike = CBLC[i];

      if (strike || (sbix && sbix[gid])) {
        if (strike && strike[2] != 17) throw "not a PNG";

        if (font["__tmp"] == null) font["__tmp"] = {};
        var cmd = font["__tmp"]["g" + gid];
        if (cmd == null) {
          var bmp, len;
          if (sbix) { bmp = sbix[gid]; len = bmp.length; }
          else {
            var boff = strike[3][gid - strike[0]] + 5;  // smallGlyphMetrics
            len = (CBDT[boff + 1] << 16) | (CBDT[boff + 2] << 8) | CBDT[boff + 3]; boff += 4;
            bmp = new Uint8Array(CBDT.buffer, CBDT.byteOffset + boff, len);
          }
          var str = ""; for (var i = 0; i < len; i++) str += String.fromCharCode(bmp[i]);
          cmd = font["__tmp"]["g" + gid] = "data:image/png;base64," + btoa(str);
        }

        path.cmds.push(cmd);
        var upe = font["head"]["unitsPerEm"] * 1.15;
        var gw = Math.round(upe), gh = Math.round(upe), dy = Math.round(-gh * 0.15);
        path.crds.push(0, gh + dy, gw, gh + dy, gw, dy, 0, dy); //*/
      }
      else if (SVG && SVG.entries[gid]) {
        var p = SVG.entries[gid];
        if (p != null) {
          if (typeof p == "number") {
            var svg = SVG.svgs[p];
            if (typeof svg == "string") {
              var prsr = new DOMParser();
              var doc = prsr["parseFromString"](svg, "image/svg+xml");
              svg = SVG.svgs[p] = doc.getElementsByTagName("svg")[0];
            }
            p = Typr["U"]["SVG"].toPath(svg, gid); SVG.entries[gid] = p;
          }
          path = p;
        }
      }
      else if (noColor != true && COLR && COLR[0]["g" + gid] && COLR[0]["g" + gid][1] > 1) {

        function toHex(n) { var o = n.toString(16); return (o.length == 1 ? "0" : "") + o; }

        var CPAL = font["CPAL"], gl = COLR[0]["g" + gid];
        for (var i = 0; i < gl[1]; i++) {
          var lid = gl[0] + i;
          var cgl = COLR[1][2 * lid], pid = COLR[1][2 * lid + 1] * 4;
          var pth = glyphToPath(font, cgl, cgl == gid);

          var col = "#" + toHex(CPAL[pid + 2]) + toHex(CPAL[pid + 1]) + toHex(CPAL[pid + 0]);
          path.cmds.push(col);

          path.cmds = path.cmds.concat(pth["cmds"]);
          path.crds = path.crds.concat(pth["crds"]);
          //console.log(gid, cgl,pid,col);

          path.cmds.push("X");
        }
      }
      else if (CFF) {
        var pdct = CFF["Private"];
        var state = { x: 0, y: 0, stack: [], nStems: 0, haveWidth: false, width: pdct ? pdct["defaultWidthX"] : 0, open: false };
        if (CFF["ROS"]) {
          var gi = 0;
          while (CFF["FDSelect"][gi + 2] <= gid) gi += 2;
          pdct = CFF["FDArray"][CFF["FDSelect"][gi + 1]]["Private"];
        }
        _drawCFF(CFF["CharStrings"][gid], state, CFF, pdct, path);
      }
      else if (font["glyf"]) { _drawGlyf(gid, font, path, axs); }
      return { "cmds": path.cmds, "crds": path.crds };
    }

    function _drawGlyf(gid, font, path, axs) {
      var gl = font["glyf"][gid];

      if (gl == null) gl = font["glyf"][gid] = Typr["T"].glyf._parseGlyf(font, gid);
      if (gl != null) {
        if (gl.noc > -1) _simpleGlyph(gl, font, gid, path, axs);
        else _compoGlyph(gl, font, gid, path, axs);
      }
    }
    function _interpolate(axs, v) {
      var S = 1;
      var s = axs[0];  // start
      var p = axs[1];  // peak
      var e = axs[2];  // end

      for (var i = 0; i < v.length; i++) {
        var AS = 1;
        if (s[i] > p[i] || p[i] > e[i]) AS = 1;
        else if (s[i] < 0 && e[i] > 0 && p[i] != 0) AS = 1;
        else if (p[i] == 0) AS = 1;
        else if (v[i] < s[i] || v[i] > e[i]) AS = 0;
        else {
          if (v[i] == p[i]) AS = 1;
          else if (v[i] < p[i]) AS = (v[i] - s[i]) / (p[i] - s[i]);
          else AS = (e[i] - v[i]) / (e[i] - p[i]);
        }
        S = S * AS;
      }
      return S;
    }
    function _normalizeAxis(font, vv) {
      var fvar = font["fvar"], avar = font["avar"];
      var fv = fvar ? fvar[0] : null;

      var nv = [];
      for (var i = 0; i < fv.length; i++) {
        var min = fv[i][1], def = fv[i][2], max = fv[i][3], v = Math.max(min, Math.min(max, vv[i]));
        if (v < def) nv[i] = (def - v) / (min - def);
        else if (v > def) nv[i] = (v - def) / (max - def);
        else nv[i] = 0;

        if (avar && nv[i] != -1) {
          var av = avar[i], j = 0;
          for (; j < av.length; j += 2) if (av[j] >= nv[i]) break;
          var f = (nv[i] - av[j - 2]) / (av[j] - av[j - 2]);
          nv[i] = f * av[j + 1] + (1 - f) * av[j - 1];
        }

      }
      return nv;
    }
    function interpolateDeltas(dfs, ind, xs, ys, endPts) {
      var N = xs.length, ndfs = new Array(N * 2 + 8); ndfs.fill(0);
      for (var i = 0; i < N; i++) {
        var dx = 0, dy = 0, ii = ind.indexOf(i);
        if (ii != -1) { dx = dfs[ii]; dy = dfs[ind.length + ii]; }
        else {
          var cmp = 0; while (endPts[cmp] < i) cmp++;
          var cmp0 = cmp == 0 ? 0 : endPts[cmp - 1] + 1;
          var cmp1 = endPts[cmp];

          var i0 = -1, i1 = -1;

          for (var j = 0; j < ind.length; j++) { var v = ind[j]; if (v < cmp0 || v > cmp1 || v >= N) continue; i0 = j; if (i1 == -1) i1 = j; }
          for (var j = 0; j < ind.length; j++) { var v = ind[j]; if (v < cmp0 || v > cmp1 || v >= N) continue; if (v < i) i0 = j; if (i < v) { i1 = j; break; } }

          //var i0 = ind.length-1, i1=0;  if(ind[i0]>=N) i0--;
          //for(var j=0; j<ind.length; j++) {  var v=ind[j];  if(v<N) { if(v<i) i0=j;  if(i<v) {  i1=j;  break;  }  }  }
          for (var ax = 0; ax < 2; ax++) {
            var crd = ax == 0 ? xs : ys, ofs = ax * ind.length, dlt = 0;
            var c0 = crd[ind[i0]], c1 = crd[ind[i1]], cC = crd[i];
            var d0 = dfs[ofs + i0], d1 = dfs[ofs + i1];

            if (c0 == c1) {
              if (d0 == d1) dlt = d0;
              else dlt = 0;
            }
            else {
              if (cC <= Math.min(c0, c1)) {
                if (c0 < c1) dlt = d0;
                else dlt = d1;
              }
              else if (Math.max(c0, c1) <= cC) {
                if (c0 < c1) dlt = d1;
                else dlt = d0;
              }
              else {
                var prop = (cC - c0) / (c1 - c0);  //if(prop<0) throw "e";
                dlt = prop * d1 + (1 - prop) * d0;
              }
            }
            if (ax == 0) dx = dlt; else dy = dlt;
          }
        }
        ndfs[i] = dx; ndfs[N + 4 + i] = dy;
      }
      return ndfs;
    }
    function _simpleGlyph(gl, font, gid, p, axs) {
      var xs = gl.xs, ys = gl.ys;
      //*
      if (font["fvar"] && axs) {
        xs = xs.slice(0); ys = ys.slice(0);
        var gvar = font["gvar"];
        var gv = gvar ? gvar[gid] : null;

        for (var vi = 0; vi < gv.length; vi++) {
          var axv = gv[vi][0];  //console.log(axs);
          var S = _interpolate(axv, axs); if (S < 1e-9) continue;
          var dfs = gv[vi][1], ind = gv[vi][2];  //if(dfs.length!=2*xs.length+8) throw "e";
          //console.log(vi,S,axv,ind,dfs);
          if (ind) { dfs = gv[vi][1] = interpolateDeltas(dfs, ind, xs, ys, gl.endPts); gv[vi][2] = null; }
          //if(ind==null)
          if (dfs.length == xs.length * 2 + 8)
            for (var i = 0; i < xs.length; i++) {
              xs[i] += S * dfs[i];
              ys[i] += S * dfs[i + xs.length + 4];
            }
        }
      } //*/

      for (var c = 0; c < gl.noc; c++) {
        var i0 = (c == 0) ? 0 : (gl.endPts[c - 1] + 1);
        var il = gl.endPts[c];

        for (var i = i0; i <= il; i++) {
          var pr = (i == i0) ? il : (i - 1);
          var nx = (i == il) ? i0 : (i + 1);
          var onCurve = gl.flags[i] & 1;
          var prOnCurve = gl.flags[pr] & 1;
          var nxOnCurve = gl.flags[nx] & 1;

          var x = xs[i], y = ys[i];

          if (i == i0) {
            if (onCurve) {
              if (prOnCurve) P.MoveTo(p, xs[pr], ys[pr]);
              else { P.MoveTo(p, x, y); continue;  /*  will do CurveTo at il  */ }
            }
            else {
              if (prOnCurve) P.MoveTo(p, xs[pr], ys[pr]);
              else P.MoveTo(p, Math.floor((xs[pr] + x) * 0.5), Math.floor((ys[pr] + y) * 0.5));
            }
          }
          if (onCurve) {
            if (prOnCurve) P.LineTo(p, x, y);
          }
          else {
            if (nxOnCurve) P.qCurveTo(p, x, y, xs[nx], ys[nx]);
            else P.qCurveTo(p, x, y, Math.floor((x + xs[nx]) * 0.5), Math.floor((y + ys[nx]) * 0.5));
          }
        }
        P.ClosePath(p);
      }
    }
    function _compoGlyph(gl, font, gid, p, axs) {

      var dx = [0, 0, 0, 0, 0, 0], dy = [0, 0, 0, 0, 0, 0], ccnt = gl.parts.length;

      if (font["fvar"] && axs) {
        var gvar = font["gvar"];
        var gv = gvar ? gvar[gid] : null;
        for (var vi = 0; vi < gv.length; vi++) {
          var axv = gv[vi][0];  //console.log(axs);
          var S = _interpolate(axv, axs); if (S < 1e-6) continue;
          var dfs = gv[vi][1], ind = gv[vi][2];  //if(dfs.length!=2*ccnt+8) throw "e";
          if (ind == null)
            for (var i = 0; i < ccnt; i++) {
              dx[i] += S * dfs[i];
              dy[i] += S * dfs[i + ccnt + 4];
            }
          else
            for (var j = 0; j < ind.length; j++) {
              var i = ind[j];
              dx[i] += S * dfs[0];
              dy[i] += S * dfs[0 + ccnt];
            }
        }
      }

      for (var j = 0; j < ccnt; j++) {
        var path = { cmds: [], crds: [] };
        var prt = gl.parts[j];
        _drawGlyf(prt.glyphIndex, font, path, axs);

        var m = prt.m, tx = m.tx + dx[j], ty = m.ty + dy[j];
        for (var i = 0; i < path.crds.length; i += 2) {
          var x = path.crds[i], y = path.crds[i + 1];
          p.crds.push(x * m.a + y * m.c + tx);   // not sure, probably right
          p.crds.push(x * m.b + y * m.d + ty);
        }
        for (var i = 0; i < path.cmds.length; i++) p.cmds.push(path.cmds[i]);
      }
    }

    function pathToSVG(path, prec) {
      var cmds = path["cmds"], crds = path["crds"];
      if (prec == null) prec = 5;
      function num(v) { return parseFloat(v.toFixed(prec)); }
      function merge(o) {
        var no = [], lstF = false, lstC = "";
        for (var i = 0; i < o.length; i++) {
          var it = o[i], isF = (typeof it) == "number";
          if (!isF) { if (it == lstC && it.length == 1 && it != "m") continue; lstC = it; }  // move should not be merged (it actually means lineTo)
          if (lstF && isF && it >= 0) no.push(" ");
          no.push(it); lstF = isF;
        }
        return no.join("");
      }


      var out = [], co = 0, lmap = { "M": 2, "L": 2, "Q": 4, "C": 6 };
      var x = 0, y = 0, // perfect coords
        //dx=0, dy=0, // relative perfect coords
        //rx=0, ry=0, // relative rounded coords
        ex = 0, ey = 0, // error between perfect and output coords
        mx = 0, my = 0; // perfect coords of the last "Move"

      for (var i = 0; i < cmds.length; i++) {
        var cmd = cmds[i], cc = (lmap[cmd] ? lmap[cmd] : 0);

        var o0 = [], dx, dy, rx, ry;  // o1=[], cx, cy, ax,ay;
        if (cmd == "L") {
          dx = crds[co] - x; dy = crds[co + 1] - y;
          rx = num(dx + ex); ry = num(dy + ey);
          // if this "lineTo" leads to the starting point, and "Z" follows, do not output anything.
          if (cmds[i + 1] == "Z" && crds[co] == mx && crds[co + 1] == my) { rx = dx; ry = dy; }
          else if (rx == 0 && ry == 0) ;
          else if (rx == 0) o0.push("v", ry);
          else if (ry == 0) o0.push("h", rx);
          else { o0.push("l", rx, ry); }
        }
        else {
          o0.push(cmd.toLowerCase());
          for (var j = 0; j < cc; j += 2) {
            dx = crds[co + j] - x; dy = crds[co + j + 1] - y;
            rx = num(dx + ex); ry = num(dy + ey);
            o0.push(rx, ry);
          }
        }
        if (cc != 0) { ex += dx - rx; ey += dy - ry; }

        var ou = o0;
        for (var j = 0; j < ou.length; j++) out.push(ou[j]);

        if (cc != 0) { co += cc; x = crds[co - 2]; y = crds[co - 1]; }
        if (cmd == "M") { mx = x; my = y; }
        if (cmd == "Z") { x = mx; y = my; }
      }

      return merge(out);
    }
    function SVGToPath(d) {
      var pth = { cmds: [], crds: [] };
      Typr["U"]["SVG"].svgToPath(d, pth);
      return { "cmds": pth.cmds, "crds": pth.crds };
    }

    function mipmapB(buff, w, h, hlp) {
      var nw = w >> 1, nh = h >> 1;
      var nbuf = new Uint8Array(nw * nh * 4);
      var sb32 = new Uint32Array(buff.buffer), nb32 = new Uint32Array(nbuf.buffer);
      for (var y = 0; y < nh; y++)
        for (var x = 0; x < nw; x++) {
          var ti = (y * nw + x), si = ((y << 1) * w + (x << 1));
          //nbuf[ti  ] = buff[si  ];  nbuf[ti+1] = buff[si+1];  nbuf[ti+2] = buff[si+2];  nbuf[ti+3] = buff[si+3];
          //*
          var c0 = sb32[si], c1 = sb32[si + 1], c2 = sb32[si + w], c3 = sb32[si + w + 1];

          var a0 = (c0 >>> 24), a1 = (c1 >>> 24), a2 = (c2 >>> 24), a3 = (c3 >>> 24), a = (a0 + a1 + a2 + a3);

          if (a == 1020) {
            var r = (((c0 >>> 0) & 255) + ((c1 >>> 0) & 255) + ((c2 >>> 0) & 255) + ((c3 >>> 0) & 255) + 2) >>> 2;
            var g = (((c0 >>> 8) & 255) + ((c1 >>> 8) & 255) + ((c2 >>> 8) & 255) + ((c3 >>> 8) & 255) + 2) >>> 2;
            var b = (((c0 >>> 16) & 255) + ((c1 >>> 16) & 255) + ((c2 >>> 16) & 255) + ((c3 >>> 16) & 255) + 2) >>> 2;
            nb32[ti] = (255 << 24) | (b << 16) | (g << 8) | r;
          }
          else if (a == 0) nb32[ti] = 0;
          else {
            var r = ((c0 >>> 0) & 255) * a0 + ((c1 >>> 0) & 255) * a1 + ((c2 >>> 0) & 255) * a2 + ((c3 >>> 0) & 255) * a3;
            var g = ((c0 >>> 8) & 255) * a0 + ((c1 >>> 8) & 255) * a1 + ((c2 >>> 8) & 255) * a2 + ((c3 >>> 8) & 255) * a3;
            var b = ((c0 >>> 16) & 255) * a0 + ((c1 >>> 16) & 255) * a1 + ((c2 >>> 16) & 255) * a2 + ((c3 >>> 16) & 255) * a3;

            var ia = 1 / a; r = ~~(r * ia + 0.5); g = ~~(g * ia + 0.5); b = ~~(b * ia + 0.5);
            nb32[ti] = (((a + 2) >>> 2) << 24) | (b << 16) | (g << 8) | r;
          }
        }
      return { buff: nbuf, w: nw, h: nh };
    }

    var __cnv, __ct;
    function pathToContext(path, ctx) {
      var c = 0, cmds = path["cmds"], crds = path["crds"];

      //ctx.translate(3500,500);  ctx.rotate(0.25);  ctx.scale(1,-1);

      for (var j = 0; j < cmds.length; j++) {
        var cmd = cmds[j];
        if (cmd == "M") {
          ctx.moveTo(crds[c], crds[c + 1]);
          c += 2;
        }
        else if (cmd == "L") {
          ctx.lineTo(crds[c], crds[c + 1]);
          c += 2;
        }
        else if (cmd == "C") {
          ctx.bezierCurveTo(crds[c], crds[c + 1], crds[c + 2], crds[c + 3], crds[c + 4], crds[c + 5]);
          c += 6;
        }
        else if (cmd == "Q") {
          ctx.quadraticCurveTo(crds[c], crds[c + 1], crds[c + 2], crds[c + 3]);
          c += 4;
        }
        else if (cmd[0] == "d") {
          var upng = window["UPNG"];
          var x0 = crds[c], y0 = crds[c + 1], x1 = crds[c + 2], y1 = crds[c + 3], x2 = crds[c + 4], y2 = crds[c + 5], x3 = crds[c + 6], y3 = crds[c + 7]; c += 8;
          //y0+=400;  y1+=400;  y1+=600;
          if (upng == null) {
            ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineTo(x3, y3); ctx.closePath();
            continue;
          }
          var dx0 = (x1 - x0), dy0 = (y1 - y0), dx1 = (x3 - x0), dy1 = (y3 - y0);
          var sbmp = atob(cmd.slice(22));
          var bmp = new Uint8Array(sbmp.length);
          for (var i = 0; i < sbmp.length; i++) bmp[i] = sbmp.charCodeAt(i);

          var img = upng["decode"](bmp.buffer), w = img["width"], h = img["height"];  //console.log(img);

          var nbmp = new Uint8Array(upng["toRGBA8"](img)[0]);
          var tr = ctx["getTransform"]();
          var scl = Math.sqrt(Math.abs(tr["a"] * tr["d"] - tr["b"] * tr["c"])) * Math.sqrt(dx1 * dx1 + dy1 * dy1) / h;
          while (scl < 0.5) {
            var nd = mipmapB(nbmp, w, h);
            nbmp = nd.buff; w = nd.w; h = nd.h; scl *= 2;
          }

          if (__cnv == null) { __cnv = document.createElement("canvas"); __ct = __cnv.getContext("2d"); }
          if (__cnv.width != w || __cnv.height != h) { __cnv.width = w; __cnv.height = h; }

          __ct.putImageData(new ImageData(new Uint8ClampedArray(nbmp.buffer), w, h), 0, 0);
          ctx.save();
          ctx.transform(dx0, dy0, dx1, dy1, x0, y0);
          ctx.scale(1 / w, 1 / h);
          ctx.drawImage(__cnv, 0, 0); //*/
          ctx.restore();
        }
        else if (cmd.charAt(0) == "#" || cmd.charAt(0) == "r") {
          ctx.beginPath();
          ctx.fillStyle = cmd;
        }
        else if (cmd.charAt(0) == "O" && cmd != "OX") {
          ctx.beginPath();
          var pts = cmd.split("-");
          ctx.lineWidth = parseFloat(pts[2]);
          ctx.lineCap = ["butt", "round", "square"][parseFloat(pts[3])];
          ctx.lineJoin = ["miter", "round", "bevel"][parseFloat(pts[4])];
          ctx.miterLimit = parseFloat(pts[5]);
          ctx.lineDashOffset = parseFloat(pts[6]);
          ctx.setLineDash(pts[7].split(",").map(parseFloat));
          ctx.strokeStyle = pts[1];
        }
        else if (cmd == "Z") {
          ctx.closePath();
        }
        else if (cmd == "X") {
          ctx.fill();
        }
        else if (cmd == "OX") {
          ctx.stroke();
        }
      }
    }


    function _drawCFF(cmds, state, font, pdct, p) {
      var stack = state.stack;
      var nStems = state.nStems, haveWidth = state.haveWidth, width = state.width, open = state.open;
      var i = 0;
      var x = state.x, y = state.y, c1x = 0, c1y = 0, c2x = 0, c2y = 0, c3x = 0, c3y = 0, c4x = 0, c4y = 0, jpx = 0, jpy = 0;
      var CFF = Typr["T"].CFF;

      var nominalWidthX = pdct["nominalWidthX"];
      var o = { val: 0, size: 0 };
      //console.log(cmds);
      while (i < cmds.length) {
        CFF.getCharString(cmds, i, o);
        var v = o.val;
        i += o.size;

        if (v == "o1" || v == "o18")  //  hstem || hstemhm
        {
          var hasWidthArg;

          // The number of stem operators on the stack is always even.
          // If the value is uneven, that means a width is specified.
          hasWidthArg = stack.length % 2 !== 0;
          if (hasWidthArg && !haveWidth) {
            width = stack.shift() + nominalWidthX;
          }

          nStems += stack.length >> 1;
          stack.length = 0;
          haveWidth = true;
        }
        else if (v == "o3" || v == "o23")  // vstem || vstemhm
        {
          var hasWidthArg;

          // The number of stem operators on the stack is always even.
          // If the value is uneven, that means a width is specified.
          hasWidthArg = stack.length % 2 !== 0;
          if (hasWidthArg && !haveWidth) {
            width = stack.shift() + nominalWidthX;
          }

          nStems += stack.length >> 1;
          stack.length = 0;
          haveWidth = true;
        }
        else if (v == "o4") {
          if (stack.length > 1 && !haveWidth) {
            width = stack.shift() + nominalWidthX;
            haveWidth = true;
          }
          if (open) P.ClosePath(p);

          y += stack.pop();
          P.MoveTo(p, x, y); open = true;
        }
        else if (v == "o5") {
          while (stack.length > 0) {
            x += stack.shift();
            y += stack.shift();
            P.LineTo(p, x, y);
          }
        }
        else if (v == "o6" || v == "o7")  // hlineto || vlineto
        {
          var count = stack.length;
          var isX = (v == "o6");

          for (var j = 0; j < count; j++) {
            var sval = stack.shift();

            if (isX) x += sval; else y += sval;
            isX = !isX;
            P.LineTo(p, x, y);
          }
        }
        else if (v == "o8" || v == "o24")	// rrcurveto || rcurveline
        {
          var count = stack.length;
          var index = 0;
          while (index + 6 <= count) {
            c1x = x + stack.shift();
            c1y = y + stack.shift();
            c2x = c1x + stack.shift();
            c2y = c1y + stack.shift();
            x = c2x + stack.shift();
            y = c2y + stack.shift();
            P.CurveTo(p, c1x, c1y, c2x, c2y, x, y);
            index += 6;
          }
          if (v == "o24") {
            x += stack.shift();
            y += stack.shift();
            P.LineTo(p, x, y);
          }
        }
        else if (v == "o11") break;
        else if (v == "o1234" || v == "o1235" || v == "o1236" || v == "o1237")//if((v+"").slice(0,3)=="o12")
        {
          if (v == "o1234") {
            c1x = x + stack.shift();    // dx1
            c1y = y;                      // dy1
            c2x = c1x + stack.shift();    // dx2
            c2y = c1y + stack.shift();    // dy2
            jpx = c2x + stack.shift();    // dx3
            jpy = c2y;                    // dy3
            c3x = jpx + stack.shift();    // dx4
            c3y = c2y;                    // dy4
            c4x = c3x + stack.shift();    // dx5
            c4y = y;                      // dy5
            x = c4x + stack.shift();      // dx6
            P.CurveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
            P.CurveTo(p, c3x, c3y, c4x, c4y, x, y);

          }
          if (v == "o1235") {
            c1x = x + stack.shift();    // dx1
            c1y = y + stack.shift();    // dy1
            c2x = c1x + stack.shift();    // dx2
            c2y = c1y + stack.shift();    // dy2
            jpx = c2x + stack.shift();    // dx3
            jpy = c2y + stack.shift();    // dy3
            c3x = jpx + stack.shift();    // dx4
            c3y = jpy + stack.shift();    // dy4
            c4x = c3x + stack.shift();    // dx5
            c4y = c3y + stack.shift();    // dy5
            x = c4x + stack.shift();      // dx6
            y = c4y + stack.shift();      // dy6
            stack.shift();                // flex depth
            P.CurveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
            P.CurveTo(p, c3x, c3y, c4x, c4y, x, y);
          }
          if (v == "o1236") {
            c1x = x + stack.shift();    // dx1
            c1y = y + stack.shift();    // dy1
            c2x = c1x + stack.shift();    // dx2
            c2y = c1y + stack.shift();    // dy2
            jpx = c2x + stack.shift();    // dx3
            jpy = c2y;                    // dy3
            c3x = jpx + stack.shift();    // dx4
            c3y = c2y;                    // dy4
            c4x = c3x + stack.shift();    // dx5
            c4y = c3y + stack.shift();    // dy5
            x = c4x + stack.shift();      // dx6
            P.CurveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
            P.CurveTo(p, c3x, c3y, c4x, c4y, x, y);
          }
          if (v == "o1237") {
            c1x = x + stack.shift();    // dx1
            c1y = y + stack.shift();    // dy1
            c2x = c1x + stack.shift();    // dx2
            c2y = c1y + stack.shift();    // dy2
            jpx = c2x + stack.shift();    // dx3
            jpy = c2y + stack.shift();    // dy3
            c3x = jpx + stack.shift();    // dx4
            c3y = jpy + stack.shift();    // dy4
            c4x = c3x + stack.shift();    // dx5
            c4y = c3y + stack.shift();    // dy5
            if (Math.abs(c4x - x) > Math.abs(c4y - y)) {
              x = c4x + stack.shift();
            } else {
              y = c4y + stack.shift();
            }
            P.CurveTo(p, c1x, c1y, c2x, c2y, jpx, jpy);
            P.CurveTo(p, c3x, c3y, c4x, c4y, x, y);
          }
        }
        else if (v == "o14") {
          if (stack.length > 0 && stack.length != 4 && !haveWidth) {
            width = stack.shift() + font["nominalWidthX"];
            haveWidth = true;
          }
          if (stack.length == 4) // seac = standard encoding accented character
          {
            var adx = stack.shift();
            var ady = stack.shift();
            var bchar = stack.shift();
            var achar = stack.shift();


            var bind = CFF.glyphBySE(font, bchar);
            var aind = CFF.glyphBySE(font, achar);

            //console.log(bchar, bind);
            //console.log(achar, aind);
            //state.x=x; state.y=y; state.nStems=nStems; state.haveWidth=haveWidth; state.width=width;  state.open=open;

            _drawCFF(font["CharStrings"][bind], state, font, pdct, p);
            state.x = adx; state.y = ady;
            _drawCFF(font["CharStrings"][aind], state, font, pdct, p);

            //x=state.x; y=state.y; nStems=state.nStems; haveWidth=state.haveWidth; width=state.width;  open=state.open;
          }
          if (open) { P.ClosePath(p); open = false; }
        }
        else if (v == "o19" || v == "o20") {
          var hasWidthArg;

          // The number of stem operators on the stack is always even.
          // If the value is uneven, that means a width is specified.
          hasWidthArg = stack.length % 2 !== 0;
          if (hasWidthArg && !haveWidth) {
            width = stack.shift() + nominalWidthX;
          }

          nStems += stack.length >> 1;
          stack.length = 0;
          haveWidth = true;

          i += (nStems + 7) >> 3;
        }

        else if (v == "o21") {
          if (stack.length > 2 && !haveWidth) {
            width = stack.shift() + nominalWidthX;
            haveWidth = true;
          }

          y += stack.pop();
          x += stack.pop();

          if (open) P.ClosePath(p);
          P.MoveTo(p, x, y); open = true;
        }
        else if (v == "o22") {
          if (stack.length > 1 && !haveWidth) {
            width = stack.shift() + nominalWidthX;
            haveWidth = true;
          }

          x += stack.pop();

          if (open) P.ClosePath(p);
          P.MoveTo(p, x, y); open = true;
        }
        else if (v == "o25") {
          while (stack.length > 6) {
            x += stack.shift();
            y += stack.shift();
            P.LineTo(p, x, y);
          }

          c1x = x + stack.shift();
          c1y = y + stack.shift();
          c2x = c1x + stack.shift();
          c2y = c1y + stack.shift();
          x = c2x + stack.shift();
          y = c2y + stack.shift();
          P.CurveTo(p, c1x, c1y, c2x, c2y, x, y);
        }
        else if (v == "o26") {
          if (stack.length % 2) {
            x += stack.shift();
          }

          while (stack.length > 0) {
            c1x = x;
            c1y = y + stack.shift();
            c2x = c1x + stack.shift();
            c2y = c1y + stack.shift();
            x = c2x;
            y = c2y + stack.shift();
            P.CurveTo(p, c1x, c1y, c2x, c2y, x, y);
          }

        }
        else if (v == "o27") {
          if (stack.length % 2) {
            y += stack.shift();
          }

          while (stack.length > 0) {
            c1x = x + stack.shift();
            c1y = y;
            c2x = c1x + stack.shift();
            c2y = c1y + stack.shift();
            x = c2x + stack.shift();
            y = c2y;
            P.CurveTo(p, c1x, c1y, c2x, c2y, x, y);
          }
        }
        else if (v == "o10" || v == "o29")	// callsubr || callgsubr
        {
          var obj = (v == "o10" ? pdct : font);
          if (stack.length == 0) { console.log("error: empty stack"); }
          else {
            var ind = stack.pop();
            var subr = obj["Subrs"][ind + obj["Bias"]];
            state.x = x; state.y = y; state.nStems = nStems; state.haveWidth = haveWidth; state.width = width; state.open = open;
            _drawCFF(subr, state, font, pdct, p);
            x = state.x; y = state.y; nStems = state.nStems; haveWidth = state.haveWidth; width = state.width; open = state.open;
          }
        }
        else if (v == "o30" || v == "o31")   // vhcurveto || hvcurveto
        {
          var count, count1 = stack.length;
          var index = 0;
          var alternate = v == "o31";

          count = count1 & -3;
          index += count1 - count;

          while (index < count) {
            if (alternate) {
              c1x = x + stack.shift();
              c1y = y;
              c2x = c1x + stack.shift();
              c2y = c1y + stack.shift();
              y = c2y + stack.shift();
              if (count - index == 5) { x = c2x + stack.shift(); index++; }
              else x = c2x;
              alternate = false;
            }
            else {
              c1x = x;
              c1y = y + stack.shift();
              c2x = c1x + stack.shift();
              c2y = c1y + stack.shift();
              x = c2x + stack.shift();
              if (count - index == 5) { y = c2y + stack.shift(); index++; }
              else y = c2y;
              alternate = true;
            }
            P.CurveTo(p, c1x, c1y, c2x, c2y, x, y);
            index += 4;
          }
        }

        else if ((v + "").charAt(0) == "o") { console.log("Unknown operation: " + v, cmds); throw v; }
        else stack.push(v);
      }
      //console.log(cmds);
      state.x = x; state.y = y; state.nStems = nStems; state.haveWidth = haveWidth; state.width = width; state.open = open;
    }

    function initHB(hurl, resp) {
      var codeLength = function (code) {
        var len = 0;
        if ((code & (0xffffffff - (1 << 7) + 1)) == 0) { len = 1; }
        else if ((code & (0xffffffff - (1 << 11) + 1)) == 0) { len = 2; }
        else if ((code & (0xffffffff - (1 << 16) + 1)) == 0) { len = 3; }
        else if ((code & (0xffffffff - (1 << 21) + 1)) == 0) { len = 4; }
        return len;
      };

      fetch(hurl)
        .then(function (x) { return x["arrayBuffer"](); })
        .then(function (ab) { return WebAssembly["instantiate"](ab); })
        .then(function (res) {
          console.log("HB ready");
          var exp = res["instance"]["exports"], mem = exp["memory"];
          //mem["grow"](30); // each page is 64kb in size
          var heapu8, u32, i32, f32;
          var __lastFnt, blob, blobPtr, face, font;

          Typr["U"]["shapeHB"] = (function () {

            var toJson = function (ptr) {
              var length = exp["hb_buffer_get_length"](ptr);
              var result = [];
              var iPtr32 = exp["hb_buffer_get_glyph_infos"](ptr, 0) >>> 2;
              var pPtr32 = exp["hb_buffer_get_glyph_positions"](ptr, 0) >>> 2;
              for (var i = 0; i < length; ++i) {
                var a = iPtr32 + i * 5, b = pPtr32 + i * 5;
                result.push({
                  "g": u32[a + 0],
                  "cl": u32[a + 2],
                  "ax": i32[b + 0],
                  "ay": i32[b + 1],
                  "dx": i32[b + 2],
                  "dy": i32[b + 3]
                });
              }
              //console.log(result);
              return result;
            };
            var te;

            return function (fnt, str, prm) {
              var fdata = fnt["_data"], fn = fnt["name"]["postScriptName"];
              var ltr = prm["ltr"], fts = prm["fts"], axs = prm["axs"];
              if (fnt["fvar"] && axs == null) axs = fnt["fvar"][1][fnt["_index"]][2];

              //var olen = mem.buffer.byteLength, nlen = 2*fdata.length+str.length*16 + 4e6;
              //if(olen<nlen) mem["grow"](((nlen-olen)>>>16)+4);  //console.log("growing",nlen);

              heapu8 = new Uint8Array(mem.buffer);
              u32 = new Uint32Array(mem.buffer);
              i32 = new Int32Array(mem.buffer);
              f32 = new Float32Array(mem.buffer);

              if (__lastFnt != fn) {
                if (blob != null) {
                  exp["hb_blob_destroy"](blob);
                  exp["free"](blobPtr);
                  exp["hb_face_destroy"](face);
                  exp["hb_font_destroy"](font);
                }
                blobPtr = exp["malloc"](fdata.byteLength); heapu8.set(fdata, blobPtr);
                blob = exp["hb_blob_create"](blobPtr, fdata.byteLength, 2, 0, 0);
                face = exp["hb_face_create"](blob, fnt["_index"]);
                font = exp["hb_font_create"](face);
                __lastFnt = fn;
              }
              if (window["TextEncoder"] == null) { alert("Your browser is too old. Please, update it."); return; }
              if (te == null) te = new window["TextEncoder"]("utf8");

              var buffer = exp["hb_buffer_create"]();
              var bytes = te["encode"](str);
              var len = bytes.length, strp = exp["malloc"](len); heapu8.set(bytes, strp);
              exp["hb_buffer_add_utf8"](buffer, strp, len, 0, len);
              exp["free"](strp);

              var bin = Typr["B"];

              var feat = 0;
              if (fts) {
                feat = exp["malloc"](16 * fts.length);
                for (var i = 0; i < fts.length; i++) {
                  var fe = fts[i];
                  var off = feat + i * 16, qo = off >>> 2;
                  bin.writeASCII(heapu8, off, fe[0].split("").reverse().join(""));
                  u32[qo + 1] = fe[1];
                  u32[qo + 2] = fe[2];
                  u32[qo + 3] = fe[3];
                }
                //console.log(fts);
              }
              var vdat = 0;
              if (axs && fnt["fvar"]) {
                var axes = fnt["fvar"][0];  //console.log(axes, axs);
                vdat = exp["malloc"](8 * axs.length);
                for (var i = 0; i < axs.length; i++) {
                  var off = vdat + i * 8, qo = off >>> 2;
                  bin.writeASCII(heapu8, off, axes[i][0].split("").reverse().join(""));
                  f32[qo + 1] = axs[i];
                }
              }
              //*/

              if (axs) exp["hb_font_set_variations"](font, vdat, axs.length);
              exp["hb_buffer_set_direction"](buffer, ltr ? 4 : 5);
              exp["hb_buffer_guess_segment_properties"](buffer);
              exp["hb_shape"](font, buffer, feat, fts ? fts.length : 0);
              var json = toJson(buffer);//buffer["json"]();
              exp["hb_buffer_destroy"](buffer);
              if (fts) exp["free"](feat);
              if (axs) exp["free"](vdat);

              var arr = json.slice(0); if (!ltr) arr.reverse();
              var ci = 0, bi = 0;  // character index, binary index
              for (var i = 1; i < arr.length; i++) {
                var gl = arr[i], cl = gl["cl"];
                while (true) {
                  var cpt = str.codePointAt(ci), cln = codeLength(cpt);
                  if (bi + cln <= cl) { bi += cln; ci += cpt <= 0xffff ? 1 : 2; }
                  else break;
                }
                //while(bi+codeLength(str.charCodeAt(ci)) <=cl) {  bi+=codeLength(str.charCodeAt(ci));  ci++;  }
                gl["cl"] = ci;
              }
              return json;
            }
          }());
          resp();
        });
    }

    return { "shape": shape, "shapeToPath": shapeToPath, "codeToGlyph": codeToGlyph, "glyphToPath": glyphToPath, "pathToSVG": pathToSVG, "SVGToPath": SVGToPath, "pathToContext": pathToContext, "initHB": initHB };
  }();

  function times(n) {
      const arr = [];
      for (let i = 0; i < n; i++) {
          arr.push(i);
      }
      return arr;
  }

  class BezierSegment {
      constructor(A, B, C, D) {
          this._totalLength = undefined;
          this.A = A;
          this.B = B;
          this.C = C;
          this.D = D;
      }
      static sampleSpacing() {
          return 2;
      }
      tangentAtParameter(parameter) {
          const t = Math.max(0, Math.min(1, parameter)); // clamp to [0, 1]
          if (t === 0 || t === 1) {
              let x;
              let y;
              if (t === 0) {
                  if (this.A.x === this.B.x && this.A.y === this.B.y) {
                      x = this.C.x - this.A.x;
                      y = this.C.y - this.A.y;
                  }
                  else {
                      x = this.B.x - this.A.x;
                      y = this.B.y - this.A.y;
                  }
              }
              else {
                  if (this.D.x === this.C.x && this.D.y === this.C.y) {
                      x = this.D.x - this.B.x;
                      y = this.D.y - this.B.y;
                  }
                  else {
                      x = this.D.x - this.C.x;
                      y = this.D.y - this.C.y;
                  }
              }
              const hypot = Math.hypot(x, y);
              if (Math.abs(hypot) > 1e-4) {
                  x /= hypot;
                  y /= hypot;
              }
              return { x, y };
          }
          const adjustedT = 1 - t;
          let x = 3 * this.D.x * Math.pow(t, 2) -
              3 * this.C.x * Math.pow(t, 2) +
              6 * this.C.x * adjustedT * t -
              6 * this.B.x * adjustedT * t +
              3 * this.B.x * Math.pow(adjustedT, 2) -
              3 * this.A.x * Math.pow(adjustedT, 2);
          let y = 3 * this.D.y * Math.pow(t, 2) -
              3 * this.C.y * Math.pow(t, 2) +
              6 * this.C.y * adjustedT * t -
              6 * this.B.y * adjustedT * t +
              3 * this.B.y * Math.pow(adjustedT, 2) -
              3 * this.A.y * Math.pow(adjustedT, 2);
          const hypot = Math.hypot(x, y);
          if (Math.abs(hypot) > 1e-4) {
              x /= hypot;
              y /= hypot;
          }
          return { x, y };
      }
      isLinear() {
          return (this.A.x === this.B.x &&
              this.A.y === this.B.y &&
              this.C.x === this.D.x &&
              this.C.y === this.D.y);
      }
      pointAtParameter(parameter) {
          const t = Math.max(0, Math.min(1, parameter)); // clamp to [0, 1]
          return {
              x: Math.pow(1 - t, 3) * this.A.x +
                  3 * Math.pow(1 - t, 2) * t * this.B.x +
                  3 * (1 - t) * Math.pow(t, 2) * this.C.x +
                  Math.pow(t, 3) * this.D.x,
              y: Math.pow(1 - t, 3) * this.A.y +
                  3 * Math.pow(1 - t, 2) * t * this.B.y +
                  3 * (1 - t) * Math.pow(t, 2) * this.C.y +
                  Math.pow(t, 3) * this.D.y,
          };
      }
      getTotalLength() {
          if (this._totalLength === undefined) {
              if (this.isLinear()) {
                  this._totalLength = Math.hypot(this.D.x - this.A.x, this.D.y - this.A.y);
              }
              else {
                  const initialSamples = Math.max(10, Math.ceil((Math.hypot(this.B.x - this.A.x, this.B.y - this.A.y) +
                      Math.hypot(this.C.x - this.B.x, this.C.y - this.B.y) +
                      Math.hypot(this.D.x - this.C.x, this.D.y - this.C.y)) /
                      BezierSegment.sampleSpacing()));
                  const pts = times(initialSamples).map((i) => this.pointAtParameter(i / (initialSamples - 1)));
                  let total = 0;
                  for (let i = 1; i < pts.length; i++) {
                      total += Math.hypot(pts[i].x - pts[i - 1].x, pts[i].y - pts[i - 1].y);
                  }
                  this._totalLength = total;
              }
          }
          return this._totalLength;
      }
  }

  class BezierPath {
      constructor(segments) {
          this.samples = [];
          this.segments = segments;
          const segmentLengths = segments.map((s) => s.getTotalLength());
          const segmentOffsets = [0];
          for (let i = 1; i < segmentLengths.length; i++) {
              segmentOffsets.push(segmentOffsets[i - 1] + segmentLengths[i - 1]);
          }
          this._totalLength =
              segmentOffsets[segmentOffsets.length - 1] +
                  segmentLengths[segmentLengths.length - 1];
          const numSegSamples = (seg) => Math.max(4, Math.ceil(seg.getTotalLength() / BezierSegment.sampleSpacing()));
          const segmentSamples = segments.map(numSegSamples);
          const numSamples = segmentSamples.reduce((acc, next) => acc + next);
          const stepSize = 1 / numSamples / 10;
          const avgDist = this._totalLength / numSamples;
          this.samples.push({
              dist: 0,
              pt: this.segments[0].A,
              tan: this.segments[0].tangentAtParameter(0),
              segIdx: 0,
              t: 0,
          });
          segments.forEach((seg, segIdx) => {
              const numSegSamples = segmentSamples[segIdx];
              // Include one extra point at the end at t = 1
              const ts = times(numSegSamples + 1).map((i) => i / numSegSamples);
              const pts = ts.map((t) => seg.pointAtParameter(t));
              let dists;
              for (let it = 0; it < 4; it++) {
                  dists = times(numSegSamples).map((i) => Math.hypot(pts[i + 1].x - pts[i].x, pts[i + 1].y - pts[i].y));
                  const distErrors = dists.map((d) => d - avgDist);
                  let offset = 0;
                  for (let i = 1; i < ts.length - 1; i++) {
                      // Shift this t value to get closer to the target length
                      offset += distErrors[i - 1];
                      ts[i] -= stepSize * offset;
                      // Sample the point at the new t value
                      pts[i] = seg.pointAtParameter(ts[i]);
                  }
              }
              let lastOffset = 0;
              pts.slice(1).forEach((pt, i) => {
                  lastOffset += dists[i];
                  this.samples.push({
                      dist: segmentOffsets[segIdx] + lastOffset,
                      pt,
                      tan: this.segments[segIdx].tangentAtParameter(ts[i + 1]),
                      segIdx,
                      t: ts[i + 1],
                  });
              });
          });
          this._jumps = [];
          this.segmentStartEnds = [{ start: 0, end: 0 }];
          for (let i = 1; i < this.samples.length; i++) {
              let prev = this.samples[i - 1];
              let next = this.samples[i];
              if (next.segIdx === prev.segIdx) {
                  this.segmentStartEnds[this.segmentStartEnds.length - 1].end = next.dist;
              }
              else {
                  const segmentStartDist = next.dist - Math.hypot(this.segments[next.segIdx].A.x - next.pt.x, this.segments[next.segIdx].A.y - next.pt.y);
                  if ((this.segments[prev.segIdx].D.x !== this.segments[next.segIdx].A.x ||
                      this.segments[prev.segIdx].D.y !== this.segments[next.segIdx].A.y)) {
                      const prevEnd = {
                          dist: segmentStartDist - 1e-8,
                          pt: this.segments[prev.segIdx].D,
                          tan: this.segments[prev.segIdx].tangentAtParameter(1),
                          segIdx: prev.segIdx,
                          t: 1,
                      };
                      const nextStart = {
                          dist: segmentStartDist + 1e-8,
                          pt: this.segments[next.segIdx].A,
                          tan: this.segments[next.segIdx].tangentAtParameter(0),
                          segIdx: next.segIdx,
                          t: 0,
                      };
                      this._jumps.push(segmentStartDist);
                      this.samples.splice(i, 0, prevEnd, nextStart);
                      this.segmentStartEnds[prevEnd.segIdx].end = prevEnd.dist;
                      this.segmentStartEnds[nextStart.segIdx] = {
                          start: nextStart.dist,
                          end: next.dist,
                      };
                      i += 2;
                  }
                  else if (next.t !== 0) {
                      const nextStart = Object.assign(Object.assign({}, next), { t: 0, pt: this.segments[next.segIdx].A, tan: this.segments[next.segIdx].tangentAtParameter(0), dist: segmentStartDist });
                      this.samples.splice(i, 0, nextStart);
                      this.segmentStartEnds[next.segIdx] = {
                          start: nextStart.dist,
                          end: next.dist,
                      };
                      i++;
                  }
                  else {
                      this.segmentStartEnds[next.segIdx] = {
                          start: next.dist,
                          end: next.dist,
                      };
                  }
              }
          }
      }
      jumps() {
          return [...this._jumps];
      }
      getTotalLength() {
          return this._totalLength;
      }
      findClosestSampleIdx(dist) {
          // Binary search to find the sample with the closest dist
          let lo = 0;
          let hi = this.samples.length - 1;
          while (lo < hi) {
              const mid = Math.floor((lo + hi) / 2);
              if (this.samples[mid].dist > dist) {
                  hi = mid - 1;
              }
              else if (this.samples[mid].dist < dist) {
                  lo = mid + 1;
              }
              else {
                  return mid;
              }
          }
          return Math.max(0, Math.min(this.samples.length - 1, Math.floor((lo + hi) / 2)));
      }
      getPointAtLength(length, approximate = false) {
          if (length <= 0)
              return this.samples[0].pt;
          if (length >= this._totalLength)
              return this.samples[this.samples.length - 1].pt;
          const idxA = this.findClosestSampleIdx(length);
          const idxB = this.samples[idxA].dist < length
              ? Math.min(idxA + 1, this.samples.length - 1)
              : Math.max(0, idxA - 1);
          const mix = Math.abs(this.samples[idxB].dist - this.samples[idxA].dist) < 1e-6
              ? 0
              : (length - this.samples[idxA].dist) /
                  (this.samples[idxB].dist - this.samples[idxA].dist);
          if (approximate || this.samples[idxA].segIdx > this.samples[idxB].segIdx) {
              // We have a set of evenly spaced samples that are close enough together
              // that we can probably just linearly interpolate between them without
              // too much loss of quality
              const x = (1 - mix) * this.samples[idxA].pt.x + mix * this.samples[idxB].pt.x;
              const y = (1 - mix) * this.samples[idxA].pt.y + mix * this.samples[idxB].pt.y;
              return { x, y };
          }
          else if (this.samples[idxA].segIdx !== this.samples[idxB].segIdx) {
              const tDiff = 1 - this.samples[idxA].t + this.samples[idxB].t;
              const mixMid = tDiff > 0 ? (1 - this.samples[idxA].t) / tDiff : 0.5;
              if (mix < mixMid) {
                  const segment = this.segments[this.samples[idxA].segIdx];
                  const mixA = mix / mixMid;
                  const t = (1 - mixA) * this.samples[idxA].t + mixA;
                  return segment.pointAtParameter(t);
              }
              else {
                  const segment = this.segments[this.samples[idxB].segIdx];
                  const mixB = (mix - mixMid) / (1 - mixMid);
                  const t = mixB * this.samples[idxB].t;
                  return segment.pointAtParameter(t);
              }
          }
          else {
              const segment = this.segments[this.samples[idxA].segIdx];
              const t = (1 - mix) * this.samples[idxA].t + mix * this.samples[idxB].t;
              return segment.pointAtParameter(t);
          }
      }
      getAngleAtLength(length, approximate = false) {
          const a = this.getTangentAtLength(length, approximate);
          const angle = Math.atan2(a.y, a.x);
          return angle;
      }
      getTangentAtLength(length, approximate = false) {
          if (length <= 0)
              return this.samples[0].tan;
          if (length >= this._totalLength)
              return this.samples[this.samples.length - 1].tan;
          const idxA = this.findClosestSampleIdx(length);
          const idxB = this.samples[idxA].dist < length
              ? Math.min(idxA + 1, this.samples.length - 1)
              : Math.max(0, idxA - 1);
          const mix = (length - this.samples[idxA].dist) /
              (this.samples[idxB].dist - this.samples[idxA].dist);
          if (approximate || this.samples[idxA].segIdx > this.samples[idxB].segIdx) {
              // We have a set of evenly spaced samples that are close enough together
              // that we can probably just linearly interpolate between them without
              // too much loss of quality
              let x = (1 - mix) * this.samples[idxA].tan.x + mix * this.samples[idxB].tan.x;
              let y = (1 - mix) * this.samples[idxA].tan.y + mix * this.samples[idxB].tan.y;
              const hypot = Math.max(Math.hypot(x, y), 1e-4);
              x /= hypot;
              y /= hypot;
              return { x, y };
          }
          else if (this.samples[idxA].segIdx !== this.samples[idxB].segIdx) {
              // Find the t value between the two samples. This is not EXACTLY the point
              // at the target distance along the path, but it's so close that it
              // is effectively the same
              if (mix < 0.5) {
                  const segment = this.segments[this.samples[idxA].segIdx];
                  const mixA = 2 * mix;
                  const t = (1 - mixA) * this.samples[idxA].t + mixA;
                  return segment.tangentAtParameter(t);
              }
              else {
                  const segment = this.segments[this.samples[idxB].segIdx];
                  const mixB = 2 * (mix - 0.5);
                  const t = mixB * this.samples[idxB].t;
                  return segment.tangentAtParameter(t);
              }
          }
          else {
              const segment = this.segments[this.samples[idxA].segIdx];
              const t = (1 - mix) * this.samples[idxA].t + mix * this.samples[idxB].t;
              return segment.tangentAtParameter(t);
          }
      }
  }
  const createFromCommands = (rawCommands) => {
      const commands = rawCommands.slice();
      if (commands.length < 2) {
          throw new Error(`Path doesn't have enough commands: ${JSON.stringify(commands)}`);
      }
      if (commands[0].type !== 'M') {
          throw new Error(`Path starts with ${commands[0].type} instead of M!`);
      }
      let lastPoint = { x: commands[0].x, y: commands[0].y };
      let firstPoint = Object.assign({}, lastPoint);
      const segments = [];
      while (commands.length > 0) {
          const command = commands.shift();
          if (command.type === 'M') {
              firstPoint = {
                  x: command.x,
                  y: command.y,
              };
              lastPoint = firstPoint;
          }
          else if (command.type === 'C') {
              segments.push(new BezierSegment(lastPoint, { x: command.x1, y: command.y1 }, { x: command.x2, y: command.y2 }, { x: command.x, y: command.y }));
              lastPoint = { x: command.x, y: command.y };
          }
          else if (command.type === 'L') {
              if (command.x !== lastPoint.x || command.y !== lastPoint.y) {
                  segments.push(new BezierSegment(lastPoint, lastPoint, { x: command.x, y: command.y }, { x: command.x, y: command.y }));
              }
              lastPoint = { x: command.x, y: command.y };
          }
          else if (command.type === 'H') {
              if (command.x !== lastPoint.x) {
                  segments.push(new BezierSegment(lastPoint, lastPoint, { x: command.x, y: lastPoint.y }, { x: command.x, y: lastPoint.y }));
              }
              lastPoint = { x: command.x, y: lastPoint.y };
          }
          else if (command.type === 'V') {
              if (command.y !== lastPoint.y) {
                  segments.push(new BezierSegment(lastPoint, lastPoint, { x: lastPoint.x, y: command.y }, { x: lastPoint.x, y: command.y }));
              }
              lastPoint = { x: lastPoint.x, y: command.y };
          }
          else if (command.type === 'Q') {
              segments.push(new BezierSegment(lastPoint, {
                  x: lastPoint.x + (2 / 3) * (command.x1 - lastPoint.x),
                  y: lastPoint.y + (2 / 3) * (command.y1 - lastPoint.y),
              }, {
                  x: command.x + (2 / 3) * (command.x1 - command.x),
                  y: command.y + (2 / 3) * (command.y1 - command.y),
              }, { x: command.x, y: command.y }));
              lastPoint = { x: command.x, y: command.y };
          }
          else if (command.type === 'Z') {
              if (Math.hypot(lastPoint.x - firstPoint.x, lastPoint.y - firstPoint.y) > 0) {
                  segments.push(new BezierSegment(lastPoint, lastPoint, firstPoint, firstPoint));
              }
          }
          else {
              throw new Error(
              // @ts-ignore
              `Unsupported path command ${command.type}; use only H, V, M, L, C, Z!`);
          }
      }
      return new BezierPath(segments);
  };

  /**
   * @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.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 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.
     *
     * @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} = 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_1.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_1.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;
        }
      }
      if (success) return success(pfont);

      return pfont;
    };
  }
  // 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);
  }

  function text(p5, fn) {
    Renderer3D.prototype.maxCachedGlyphs = function() {
      // TODO: use more than vibes to find a good value for this
      return 200;
    };

    Font.prototype._getFontInfo = function(axs) {
      // For WebGL, a cache of font data to use on the GPU.
      this._fontInfos = this._fontInfos || {};

      const key = JSON.stringify(axs);
      if (this._fontInfos[key]) {
        const val = this._fontInfos[key];
        return val;
      } else {
        const val = new FontInfo(this, { axs });
        this._fontInfos[key] = val;
        return val;
      }
    };

    // rendering constants

    // the number of rows/columns dividing each glyph
    const charGridWidth = 9;
    const charGridHeight = charGridWidth;

    // size of the image holding the bezier stroke info
    const strokeImageWidth = 64;
    const strokeImageHeight = 64;

    // size of the image holding the stroke indices for each row/col
    const gridImageWidth = 64;
    const gridImageHeight = 64;

    // size of the image holding the offset/length of each row/col stripe
    const cellImageWidth = 64;
    const cellImageHeight = 64;

    /**
     * @private
     * @class ImageInfos
     * @param {Integer} width
     * @param {Integer} height
     *
     * the ImageInfos class holds a list of ImageDatas of a given size.
     */
    class ImageInfos {
      constructor(width, height) {
        this.width = width;
        this.height = height;
        this.infos = []; // the list of images
      }
      /**
       *
       * @param {Integer} space
       * @return {Object} contains the ImageData, and pixel index into that
       *                  ImageData where the free space was allocated.
       *
       * finds free space of a given size in the ImageData list
       */
      findImage(space) {
        const imageSize = this.width * this.height;
        if (space > imageSize)
          throw new Error('font is too complex to render in 3D');

        // search through the list of images, looking for one with
        // anough unused space.
        let imageInfo, imageData;
        for (let ii = this.infos.length - 1; ii >= 0; --ii) {
          const imageInfoTest = this.infos[ii];
          if (imageInfoTest.index + space < imageSize) {
            // found one
            imageInfo = imageInfoTest;
            imageData = imageInfoTest.imageData;
            break;
          }
        }

        if (!imageInfo) {
          try {
            // create a new image
            imageData = new ImageData(this.width, this.height);
          } catch (err) {
            // for browsers that don't support ImageData constructors (ie IE11)
            // create an ImageData using the old method
            let canvas = document.getElementsByTagName('canvas')[0];
            const created = !canvas;
            if (!canvas) {
              // create a temporary canvas
              canvas = document.createElement('canvas');
              canvas.style.display = 'none';
              document.body.appendChild(canvas);
            }
            const ctx = canvas.getContext('2d');
            if (ctx) {
              imageData = ctx.createImageData(this.width, this.height);
            }
            if (created) {
              // distroy the temporary canvas, if necessary
              document.body.removeChild(canvas);
            }
          }
          // construct & dd the new image info
          imageInfo = { index: 0, imageData };
          this.infos.push(imageInfo);
        }

        const index = imageInfo.index;
        imageInfo.index += space; // move to the start of the next image
        imageData._dirty = true;
        return { imageData, index };
      }
    }

    /**
     * @function setPixel
     * @private
     * @param {Object} imageInfo
     * @param {Number} r
     * @param {Number} g
     * @param {Number} b
     * @param {Number} a
     *
     * writes the next pixel into an indexed ImageData
     */
    function setPixel(imageInfo, r, g, b, a) {
      const imageData = imageInfo.imageData;
      const pixels = imageData.data;
      let index = imageInfo.index++ * 4;
      pixels[index++] = r;
      pixels[index++] = g;
      pixels[index++] = b;
      pixels[index++] = a;
    }

    const SQRT3 = Math.sqrt(3);

    /**
     * @private
     * @class FontInfo
     * @param {Object} font an opentype.js font object
     *
     * contains cached images and glyph information for an opentype font
     */
    class FontInfo {
      constructor(font, { axs } = {}) {
        this.font = font;
        this.axs = axs;
        // the bezier curve coordinates
        this.strokeImageInfos = new ImageInfos(
          strokeImageWidth,
          strokeImageHeight
        );
        // lists of curve indices for each row/column slice
        this.colDimImageInfos = new ImageInfos(gridImageWidth, gridImageHeight);
        this.rowDimImageInfos = new ImageInfos(gridImageWidth, gridImageHeight);
        // the offset & length of each row/col slice in the glyph
        this.colCellImageInfos = new ImageInfos(cellImageWidth, cellImageHeight);
        this.rowCellImageInfos = new ImageInfos(cellImageWidth, cellImageHeight);

        // the cached information for each glyph
        this.glyphInfos = {};
      }
      /**
       * @param {Glyph} glyph the x positions of points in the curve
       * @returns {Object} the glyphInfo for that glyph
       *
       * calculates rendering info for a glyph, including the curve information,
       * row & column stripes compiled into textures.
       */
      getGlyphInfo(glyph) {
        // check the cache
        let gi = this.glyphInfos[glyph.index];
        if (gi) return gi;

        const axs = this.axs;
        const {
          glyph: {
            path: { commands }
          }
        } = this.font._singleShapeToPath(glyph.shape, { axs });
        let xMin = Infinity;
        let xMax = -Infinity;
        let yMin = Infinity;
        let yMax = -Infinity;

        for (const cmd of commands) {
          for (let i = 1; i < cmd.length; i += 2) {
            xMin = Math.min(xMin, cmd[i]);
            xMax = Math.max(xMax, cmd[i]);
            yMin = Math.min(yMin, cmd[i + 1]);
            yMax = Math.max(yMax, cmd[i + 1]);
          }
        }

        // don't bother rendering invisible glyphs
        if (xMin >= xMax || yMin >= yMax || !commands.length) {
          return (this.glyphInfos[glyph.index] = {});
        }

        const gWidth = xMax - xMin;
        const gHeight = yMax - yMin;

        // Convert arrays to named objects
        const cmds = arrayCommandsToObjects(commands);

        let i;
        const strokes = []; // the strokes in this glyph
        const rows = []; // the indices of strokes in each row
        const cols = []; // the indices of strokes in each column
        for (i = charGridWidth - 1; i >= 0; --i) cols.push([]);
        for (i = charGridHeight - 1; i >= 0; --i) rows.push([]);

        /**
         * @function push
         * @private
         * @param {Number[]} xs the x positions of points in the curve
         * @param {Number[]} ys the y positions of points in the curve
         * @param {Object} v    the curve information
         *
         * adds a curve to the rows & columns that it intersects with
         */
        function push(xs, ys, v) {
          const index = strokes.length; // the index of this stroke
          strokes.push(v); // add this stroke to the list

          /**
           * @function minMax
           * @private
           * @param {Number[]} rg the list of values to compare
           * @param {Number} min the initial minimum value
           * @param {Number} max the initial maximum value
           *
           * find the minimum & maximum value in a list of values
           */
          function minMax(rg, min, max) {
            for (let i = rg.length; i-- > 0; ) {
              const v = rg[i];
              if (min > v) min = v;
              if (max < v) max = v;
            }
            return { min, max };
          }

          // Expand the bounding box of the glyph by the number of cells below
          // before rounding. Curves only partially through a cell won't be added
          // to adjacent cells, but ones that are close will be. This helps fix
          // small visual glitches that occur when curves are close to grid cell
          // boundaries.
          const cellOffset = 0.5;

          // loop through the rows & columns that the curve intersects
          // adding the curve to those slices
          const mmX = minMax(xs, 1, 0);
          const ixMin = Math.max(
            Math.floor(mmX.min * charGridWidth - cellOffset),
            0
          );
          const ixMax = Math.min(
            Math.ceil(mmX.max * charGridWidth + cellOffset),
            charGridWidth
          );
          for (let iCol = ixMin; iCol < ixMax; ++iCol) cols[iCol].push(index);

          const mmY = minMax(ys, 1, 0);
          const iyMin = Math.max(
            Math.floor(mmY.min * charGridHeight - cellOffset),
            0
          );
          const iyMax = Math.min(
            Math.ceil(mmY.max * charGridHeight + cellOffset),
            charGridHeight
          );
          for (let iRow = iyMin; iRow < iyMax; ++iRow) rows[iRow].push(index);
        }

        /**
         * @function clamp
         * @private
         * @param {Number} v the value to clamp
         * @param {Number} min the minimum value
         * @param {Number} max the maxmimum value
         *
         * clamps a value between a minimum & maximum value
         */
        function clamp(v, min, max) {
          if (v < min) return min;
          if (v > max) return max;
          return v;
        }

        /**
         * @function byte
         * @private
         * @param {Number} v the value to scale
         *
         * converts a floating-point number in the range 0-1 to a byte 0-255
         */
        function byte(v) {
          return clamp(255 * v, 0, 255);
        }

        /**
         * @private
         * @class Cubic
         * @param {Number} p0 the start point of the curve
         * @param {Number} c0 the first control point
         * @param {Number} c1 the second control point
         * @param {Number} p1 the end point
         *
         * a cubic curve
         */
        class Cubic {
          constructor(p0, c0, c1, p1) {
            this.p0 = p0;
            this.c0 = c0;
            this.c1 = c1;
            this.p1 = p1;
          }
          /**
           * @return {Object} the quadratic approximation
           *
           * converts the cubic to a quadtratic approximation by
           * picking an appropriate quadratic control point
           */
          toQuadratic() {
            return {
              x: this.p0.x,
              y: this.p0.y,
              x1: this.p1.x,
              y1: this.p1.y,
              cx: ((this.c0.x + this.c1.x) * 3 - (this.p0.x + this.p1.x)) / 4,
              cy: ((this.c0.y + this.c1.y) * 3 - (this.p0.y + this.p1.y)) / 4
            };
          }

          /**
           * @return {Number} the error
           *
           * calculates the magnitude of error of this curve's
           * quadratic approximation.
           */
          quadError() {
            return (
              Vector.sub(
                Vector.sub(this.p1, this.p0),
                Vector.mult(Vector.sub(this.c1, this.c0), 3)
              ).mag() / 2
            );
          }

          /**
           * @param {Number} t the value (0-1) at which to split
           * @return {Cubic} the second part of the curve
           *
           * splits the cubic into two parts at a point 't' along the curve.
           * this cubic keeps its start point and its end point becomes the
           * point at 't'. the 'end half is returned.
           */
          split(t) {
            const m1 = Vector.lerp(this.p0, this.c0, t);
            const m2 = Vector.lerp(this.c0, this.c1, t);
            const mm1 = Vector.lerp(m1, m2, t);

            this.c1 = Vector.lerp(this.c1, this.p1, t);
            this.c0 = Vector.lerp(m2, this.c1, t);
            const pt = Vector.lerp(mm1, this.c0, t);
            const part1 = new Cubic(this.p0, m1, mm1, pt);
            this.p0 = pt;
            return part1;
          }

          /**
           * @return {Cubic[]} the non-inflecting pieces of this cubic
           *
           * returns an array containing 0, 1 or 2 cubics split resulting
           * from splitting this cubic at its inflection points.
           * this cubic is (potentially) altered and returned in the list.
           */
          splitInflections() {
            const a = Vector.sub(this.c0, this.p0);
            const b = Vector.sub(Vector.sub(this.c1, this.c0), a);
            const c = Vector.sub(
              Vector.sub(Vector.sub(this.p1, this.c1), a),
              Vector.mult(b, 2)
            );

            const cubics = [];

            // find the derivative coefficients
            let A = b.x * c.y - b.y * c.x;
            if (A !== 0) {
              let B = a.x * c.y - a.y * c.x;
              let C = a.x * b.y - a.y * b.x;
              const disc = B * B - 4 * A * C;
              if (disc >= 0) {
                if (A < 0) {
                  A = -A;
                  B = -B;
                  C = -C;
                }

                const Q = Math.sqrt(disc);
                const t0 = (-B - Q) / (2 * A); // the first inflection point
                let t1 = (-B + Q) / (2 * A); // the second inflection point

                // test if the first inflection point lies on the curve
                if (t0 > 0 && t0 < 1) {
                  // split at the first inflection point
                  cubics.push(this.split(t0));
                  // scale t2 into the second part
                  t1 = 1 - (1 - t1) / (1 - t0);
                }

                // test if the second inflection point lies on the curve
                if (t1 > 0 && t1 < 1) {
                  // split at the second inflection point
                  cubics.push(this.split(t1));
                }
              }
            }

            cubics.push(this);
            return cubics;
          }
        }

        /**
         * @function cubicToQuadratics
         * @private
         * @param {Number} x0
         * @param {Number} y0
         * @param {Number} cx0
         * @param {Number} cy0
         * @param {Number} cx1
         * @param {Number} cy1
         * @param {Number} x1
         * @param {Number} y1
         * @returns {Cubic[]} an array of cubics whose quadratic approximations
         *                    closely match the civen cubic.
         *
         * converts a cubic curve to a list of quadratics.
         */
        function cubicToQuadratics(x0, y0, cx0, cy0, cx1, cy1, x1, y1) {
          // create the Cubic object and split it at its inflections
          const cubics = new Cubic(
            new Vector(x0, y0),
            new Vector(cx0, cy0),
            new Vector(cx1, cy1),
            new Vector(x1, y1)
          ).splitInflections();

          const qs = []; // the final list of quadratics
          const precision = 30 / SQRT3;

          // for each of the non-inflected pieces of the original cubic
          for (let cubic of cubics) {
            // the cubic is iteratively split in 3 pieces:
            // the first piece is accumulated in 'qs', the result.
            // the last piece is accumulated in 'tail', temporarily.
            // the middle piece is repeatedly split again, while necessary.
            const tail = [];

            let t3;
            for (;;) {
              // calculate this cubic's precision
              t3 = precision / cubic.quadError();
              if (t3 >= 0.5 * 0.5 * 0.5) {
                break; // not too bad, we're done
              }

              // find a split point based on the error
              const t = Math.pow(t3, 1.0 / 3.0);
              // split the cubic in 3
              const start = cubic.split(t);
              const middle = cubic.split(1 - t / (1 - t));

              qs.push(start); // the first part
              tail.push(cubic); // the last part
              cubic = middle; // iterate on the middle piece
            }

            if (t3 < 1) {
              // a little excess error, split the middle in two
              qs.push(cubic.split(0.5));
            }
            // add the middle piece to the result
            qs.push(cubic);

            // finally add the tail, reversed, onto the result
            Array.prototype.push.apply(qs, tail.reverse());
          }

          return qs;
        }

        /**
         * @function pushLine
         * @private
         * @param {Number} x0
         * @param {Number} y0
         * @param {Number} x1
         * @param {Number} y1
         *
         * add a straight line to the row/col grid of a glyph
         */
        function pushLine(x0, y0, x1, y1) {
          const mx = (x0 + x1) / 2;
          const my = (y0 + y1) / 2;
          push([x0, x1], [y0, y1], { x: x0, y: y0, cx: mx, cy: my });
        }

        /**
         * @function samePoint
         * @private
         * @param {Number} x0
         * @param {Number} y0
         * @param {Number} x1
         * @param {Number} y1
         * @return {Boolean} true if the two points are sufficiently close
         *
         * tests if two points are close enough to be considered the same
         */
        function samePoint(x0, y0, x1, y1) {
          return Math.abs(x1 - x0) < 0.00001 && Math.abs(y1 - y0) < 0.00001;
        }

        let x0, y0, xs, ys;

        for (const cmd of cmds) {
          // scale the coordinates to the range 0-1
          const x1 = (cmd.x - xMin) / gWidth;
          const y1 = (cmd.y - yMin) / gHeight;

          // don't bother if this point is the same as the last
          if (samePoint(x0, y0, x1, y1)) continue;

          switch (cmd.type) {
            case 'M': {
              // move
              xs = x1;
              ys = y1;
              break;
            }
            case 'L': {
              // line
              pushLine(x0, y0, x1, y1);
              break;
            }
            case 'Q': {
              // quadratic
              const cx = (cmd.x1 - xMin) / gWidth;
              const cy = (cmd.y1 - yMin) / gHeight;
              push([x0, x1, cx], [y0, y1, cy], { x: x0, y: y0, cx, cy });
              break;
            }
            case 'Z': {
              // end
              if (!samePoint(x0, y0, xs, ys)) {
                // add an extra line closing the loop, if necessary
                pushLine(x0, y0, xs, ys);
                strokes.push({ x: xs, y: ys });
              } else {
                strokes.push({ x: x0, y: y0 });
              }
              break;
            }
            case 'C': {
              // cubic
              const cx1 = (cmd.x1 - xMin) / gWidth;
              const cy1 = (cmd.y1 - yMin) / gHeight;
              const cx2 = (cmd.x2 - xMin) / gWidth;
              const cy2 = (cmd.y2 - yMin) / gHeight;
              const qs = cubicToQuadratics(x0, y0, cx1, cy1, cx2, cy2, x1, y1);
              for (let iq = 0; iq < qs.length; iq++) {
                const q = qs[iq].toQuadratic();
                push([q.x, q.x1, q.cx], [q.y, q.y1, q.cy], q);
              }
              break;
            }
            default:
              throw new Error(`unknown command type: ${cmd.type}`);
          }
          x0 = x1;
          y0 = y1;
        }

        // allocate space for the strokes
        const strokeCount = strokes.length;
        const strokeImageInfo = this.strokeImageInfos.findImage(strokeCount);
        const strokeOffset = strokeImageInfo.index;

        // fill the stroke image
        for (let il = 0; il < strokeCount; ++il) {
          const s = strokes[il];
          setPixel(strokeImageInfo, byte(s.x), byte(s.y), byte(s.cx), byte(s.cy));
        }

        /**
         * @function layout
         * @private
         * @param {Number[][]} dim
         * @param {ImageInfos} dimImageInfos
         * @param {ImageInfos} cellImageInfos
         * @return {Object}
         *
         * lays out the curves in a dimension (row or col) into two
         * images, one for the indices of the curves themselves, and
         * one containing the offset and length of those index spans.
         */
        function layout(dim, dimImageInfos, cellImageInfos) {
          const dimLength = dim.length; // the number of slices in this dimension
          const dimImageInfo = dimImageInfos.findImage(dimLength);
          const dimOffset = dimImageInfo.index;
          // calculate the total number of stroke indices in this dimension
          let totalStrokes = 0;
          for (let id = 0; id < dimLength; ++id) {
            totalStrokes += dim[id].length;
          }

          // allocate space for the stroke indices
          const cellImageInfo = cellImageInfos.findImage(totalStrokes);

          // for each slice in the glyph
          for (let i = 0; i < dimLength; ++i) {
            const strokeIndices = dim[i];
            const strokeCount = strokeIndices.length;
            const cellLineIndex = cellImageInfo.index;

            // write the offset and count into the glyph slice image
            setPixel(
              dimImageInfo,
              cellLineIndex >> 7,
              cellLineIndex & 0x7f,
              strokeCount >> 7,
              strokeCount & 0x7f
            );

            // for each stroke index in that slice
            for (let iil = 0; iil < strokeCount; ++iil) {
              // write the stroke index into the slice's image
              const strokeIndex = strokeIndices[iil] + strokeOffset;
              setPixel(cellImageInfo, strokeIndex >> 7, strokeIndex & 0x7f, 0, 0);
            }
          }

          return {
            cellImageInfo,
            dimOffset,
            dimImageInfo
          };
        }

        // initialize the info for this glyph
        gi = this.glyphInfos[glyph.index] = {
          glyph,
          uGlyphRect: [xMin, yMin, xMax, yMax],
          strokeImageInfo,
          strokes,
          colInfo: layout(cols, this.colDimImageInfos, this.colCellImageInfos),
          rowInfo: layout(rows, this.rowDimImageInfos, this.rowCellImageInfos)
        };
        gi.uGridOffset = [gi.colInfo.dimOffset, gi.rowInfo.dimOffset];
        return gi;
      }
    }

    Renderer3D.prototype._renderText = function (line, x, y, maxY, minY) {
      if (!this.states.textFont || typeof this.states.textFont === 'string') {
        console.log(
          'WEBGL: you must load and set a font before drawing text. See `loadFont` and `textFont` for more details.'
        );
        return;
      }
      if (y >= maxY || !this.states.fillColor) {
        return; // don't render lines beyond our maxY position
      }

      if (!p5.Font.hasGlyphData(this.states.textFont)) {
        console.log(
          'WEBGL: only Opentype (.otf) and Truetype (.ttf) fonts with glyph data are supported'
        );
        return;
      }

      this.push(); // fix to #803

      // remember this state, so it can be restored later
      const doStroke = this.states.strokeColor;
      const drawMode = this.states.drawMode;

      this.states.setValue('strokeColor', null);
      this.states.setValue('drawMode', TEXTURE);

      // get the cached FontInfo object
      const { font } = this.states.textFont;
      if (!font) {
        throw new Error(
          'In WebGL mode, textFont() needs to be given the result of loadFont() instead of a font family name.'
        );
      }
      const axs = font._currentAxes(this);
      let fontInfo = font._getFontInfo(axs);

      // calculate the alignment and move/scale the view accordingly
      // TODO: check this
      const pos = { x, y }; // this.states.textFont._handleAlignment(this, line, x, y);
      const fontSize = this.states.textSize;
      const scale = fontSize / (font.data?.head?.unitsPerEm || 1000);
      this.translate(pos.x, pos.y, 0);
      this.scale(scale, scale, 1);

      // initialize the font shader
      const initializeShader = !this._defaultFontShader;
      const sh = this._getFontShader();
      sh.init();
      sh.bindShader('text'); // first time around, bind the shader fully

      if (initializeShader) {
        // these are constants, really. just initialize them one-time.
        sh.setUniform('uGridImageSize', [gridImageWidth, gridImageHeight]);
        sh.setUniform('uCellsImageSize', [cellImageWidth, cellImageHeight]);
        sh.setUniform('uStrokeImageSize', [strokeImageWidth, strokeImageHeight]);
        sh.setUniform('uGridSize', [charGridWidth, charGridHeight]);
      }

      const curFillColor = this.states.fillSet
        ? this.states.curFillColor
        : [0, 0, 0, 1];

      this._setGlobalUniforms(sh);
      this._applyColorBlend(curFillColor);

      let g = this.geometryBufferCache.getGeometryByID('glyph');
      if (!g) {
        // create the geometry for rendering a quad
        g = this._textGeom = new Geometry(
          1,
          1,
          function () {
            for (let i = 0; i <= 1; i++) {
              for (let j = 0; j <= 1; j++) {
                this.vertices.push(new Vector(j, i, 0));
                this.uvs.push(j, i);
              }
            }
          },
          this
        );
        g.gid = 'glyph';
        g.computeFaces().computeNormals();
        this.geometryBufferCache.ensureCached(g);
      }

      // bind the shader buffers
      for (const buff of this.buffers.text) {
        buff._prepareBuffer(g, sh);
      }

      // this will have to do for now...
      sh.setUniform('uMaterialColor', curFillColor);
      this._disableRemainingAttributes(sh);
      this._beforeDrawText();
      this.glyphDataCache = this.glyphDataCache || new Set();

      try {
        // fetch the glyphs in the line of text
        const glyphs = font._positionGlyphs(line);

        for (const glyph of glyphs) {
          const gi = fontInfo.getGlyphInfo(glyph);
          if (gi.uGlyphRect) {
            const rowInfo = gi.rowInfo;
            const colInfo = gi.colInfo;

            // Bump the resources for this glyph to the end of the cache list by deleting and re-adding
            const glyphResources = [
              gi.strokeImageInfo.imageData,
              rowInfo.cellImageInfo.imageData,
              rowInfo.dimImageInfo.imageData,
              colInfo.cellImageInfo.imageData,
              colInfo.dimImageInfo.imageData
            ];
            for (const resource of glyphResources) {
              this.glyphDataCache.delete(resource);
              this.glyphDataCache.add(resource);
            }

            // If we have too many glyph textures, remove the least recently used
            // ones from GPU memory. The data still exists on the CPU and will be
            // re-uploaded if it gets actively used again.
            while (this.glyphDataCache.size > this.maxCachedGlyphs()) {
              const data = this.glyphDataCache.values().next().value;
              this.glyphDataCache.delete(data);
              const tex = this.textures.get(data);
              if (tex) {
                tex.remove();
                this.textures.delete(data);
              }
            }

            sh.setUniform('uSamplerStrokes', gi.strokeImageInfo.imageData);
            sh.setUniform('uSamplerRowStrokes', rowInfo.cellImageInfo.imageData);
            sh.setUniform('uSamplerRows', rowInfo.dimImageInfo.imageData);
            sh.setUniform('uSamplerColStrokes', colInfo.cellImageInfo.imageData);
            sh.setUniform('uSamplerCols', colInfo.dimImageInfo.imageData);
            sh.setUniform('uGridOffset', gi.uGridOffset);
            sh.setUniform('uGlyphRect', gi.uGlyphRect);
            sh.setUniform('uGlyphOffset', glyph.x);

            sh.bindTextures(); // afterwards, only textures need updating

            // draw it
            this._drawBuffers(g, { mode: TRIANGLES, count: 1 });
          }
        }
      } finally {
        // clean up
        sh.unbindShader();

        this.states.setValue('strokeColor', doStroke);
        this.states.setValue('drawMode', drawMode);

        this._afterDrawText();
        this.pop();
      }
    };
  }

  var lightingShader = "#define PI 3.141592\n\nprecision highp float;\nprecision highp int;\n\nuniform mat4 uViewMatrix;\nuniform mat3 uCameraNormalMatrix;\n\nuniform bool uUseLighting;\n\nuniform int uDirectionalLightCount;\nuniform vec3 uLightingDirection[5];\nuniform vec3 uDirectionalDiffuseColors[5];\nuniform vec3 uDirectionalSpecularColors[5];\n\nuniform int uPointLightCount;\nuniform vec3 uPointLightLocation[5];\nuniform vec3 uPointLightDiffuseColors[5];\t\nuniform vec3 uPointLightSpecularColors[5];\n\nuniform int uSpotLightCount;\nuniform float uSpotLightAngle[5];\nuniform float uSpotLightConc[5];\nuniform vec3 uSpotLightDiffuseColors[5];\nuniform vec3 uSpotLightSpecularColors[5];\nuniform vec3 uSpotLightLocation[5];\nuniform vec3 uSpotLightDirection[5];\n\nuniform bool uSpecular;\nuniform float uShininess;\nuniform float uMetallic;\n\nuniform float uConstantAttenuation;\nuniform float uLinearAttenuation;\nuniform float uQuadraticAttenuation;\n\n// setting from  _setImageLightUniforms()\n// boolean to initiate the calculateImageDiffuse and calculateImageSpecular\nuniform bool uUseImageLight;\n// texture for use in calculateImageDiffuse\nuniform sampler2D environmentMapDiffused;\n// texture for use in calculateImageSpecular\nuniform sampler2D environmentMapSpecular;\n\nconst float specularFactor = 2.0;\nconst float diffuseFactor = 0.73;\n\nstruct LightResult {\n  float specular;\n  float diffuse;\n};\n\nfloat _phongSpecular(\n  vec3 lightDirection,\n  vec3 viewDirection,\n  vec3 surfaceNormal,\n  float shininess) {\n\n  vec3 R = reflect(lightDirection, surfaceNormal);\n  return pow(max(0.0, dot(R, viewDirection)), shininess);\n}\n\nfloat _lambertDiffuse(vec3 lightDirection, vec3 surfaceNormal) {\n  return max(0.0, dot(-lightDirection, surfaceNormal));\n}\n\nLightResult _light(vec3 viewDirection, vec3 normal, vec3 lightVector, float shininess, float metallic) {\n\n  vec3 lightDir = normalize(lightVector);\n\n  //compute our diffuse & specular terms\n  LightResult lr;\n  float specularIntensity = mix(1.0, 0.4, metallic);\n  float diffuseIntensity = mix(1.0, 0.1, metallic);\n  if (uSpecular)\n    lr.specular = (_phongSpecular(lightDir, viewDirection, normal, shininess)) * specularIntensity;\n    lr.diffuse = _lambertDiffuse(lightDir, normal) * diffuseIntensity;\n  return lr;\n}\n\n// converts the range of \"value\" from [min1 to max1] to [min2 to max2]\nfloat map(float value, float min1, float max1, float min2, float max2) {\n  return min2 + (value - min1) * (max2 - min2) / (max1 - min1);\n}\n\nvec2 mapTextureToNormal( vec3 v ){\n  // x = r sin(phi) cos(theta)   \n  // y = r cos(phi)  \n  // z = r sin(phi) sin(theta)\n  float phi = acos( v.y );\n  // if phi is 0, then there are no x, z components\n  float theta = 0.0;\n  // else \n  theta = acos(v.x / sin(phi));\n  float sinTheta = v.z / sin(phi);\n  if (sinTheta < 0.0) {\n    // Turn it into -theta, but in the 0-2PI range\n    theta = 2.0 * PI - theta;\n  }\n  theta = theta / (2.0 * 3.14159);\n  phi = phi / 3.14159 ;\n  \n  vec2 angles = vec2( fract(theta + 0.25), 1.0 - phi );\n  return angles;\n}\n\n\nvec3 calculateImageDiffuse(vec3 vNormal, vec3 vViewPosition, float metallic){\n  // make 2 seperate builds \n  vec3 worldCameraPosition =  vec3(0.0, 0.0, 0.0);  // hardcoded world camera position\n  vec3 worldNormal = normalize(vNormal * uCameraNormalMatrix);\n  vec2 newTexCoor = mapTextureToNormal( worldNormal );\n  vec4 texture = TEXTURE( environmentMapDiffused, newTexCoor );\n  // this is to make the darker sections more dark\n  // png and jpg usually flatten the brightness so it is to reverse that\n  return mix(smoothstep(vec3(0.0), vec3(1.0), texture.xyz), vec3(0.0), metallic);\n}\n\nvec3 calculateImageSpecular(vec3 vNormal, vec3 vViewPosition, float shininess, float metallic){\n  vec3 worldCameraPosition =  vec3(0.0, 0.0, 0.0);\n  vec3 worldNormal = normalize(vNormal);\n  vec3 lightDirection = normalize( vViewPosition - worldCameraPosition );\n  vec3 R = reflect(lightDirection, worldNormal) * uCameraNormalMatrix;\n  vec2 newTexCoor = mapTextureToNormal( R );\n#ifdef WEBGL2\n  // In p5js the range of shininess is >= 1,\n  // Therefore roughness range will be ([0,1]*8)*20 or [0, 160]\n  // The factor of 8 is because currently the getSpecularTexture\n  // only calculated 8 different levels of roughness\n  // The factor of 20 is just to spread up this range so that,\n  // [1, max] of shininess is converted to [0,160] of roughness\n  float roughness = 20. / shininess;\n  vec4 outColor = textureLod(environmentMapSpecular, newTexCoor, roughness * 8.);\n#else\n  vec4 outColor = TEXTURE(environmentMapSpecular, newTexCoor);\n#endif\n  // this is to make the darker sections more dark\n  // png and jpg usually flatten the brightness so it is to reverse that\n  return mix(\n    pow(outColor.xyz, vec3(10)),\n    pow(outColor.xyz, vec3(1.2)),\n    metallic \n  );\n}\n\nvoid totalLight(\n  vec3 modelPosition,\n  vec3 normal,\n  float shininess,\n  float metallic,\n  out vec3 totalDiffuse,\n  out vec3 totalSpecular\n) {\n\n  totalSpecular = vec3(0.0);\n\n  if (!uUseLighting) {\n    totalDiffuse = vec3(1.0);\n    return;\n  }\n\n  totalDiffuse = vec3(0.0);\n\n  vec3 viewDirection = normalize(-modelPosition);\n\n  for (int j = 0; j < 5; j++) {\n    if (j < uDirectionalLightCount) {\n      vec3 lightVector = (uViewMatrix * vec4(uLightingDirection[j], 0.0)).xyz;\n      vec3 lightColor = uDirectionalDiffuseColors[j];\n      vec3 specularColor = uDirectionalSpecularColors[j];\n      LightResult result = _light(viewDirection, normal, lightVector, shininess, metallic);\n      totalDiffuse += result.diffuse * lightColor;\n      totalSpecular += result.specular * lightColor * specularColor;\n    }\n\n    if (j < uPointLightCount) {\n      vec3 lightPosition = (uViewMatrix * vec4(uPointLightLocation[j], 1.0)).xyz;\n      vec3 lightVector = modelPosition - lightPosition;\n      //calculate attenuation\n      float lightDistance = length(lightVector);\n      float lightFalloff = 1.0 / (uConstantAttenuation + lightDistance * uLinearAttenuation + (lightDistance * lightDistance) * uQuadraticAttenuation);\n      vec3 lightColor = lightFalloff * uPointLightDiffuseColors[j];\n      vec3 specularColor = lightFalloff * uPointLightSpecularColors[j];\n\n      LightResult result = _light(viewDirection, normal, lightVector, shininess, metallic);\n      totalDiffuse += result.diffuse * lightColor;\n      totalSpecular += result.specular * lightColor * specularColor;\n    }\n\n    if(j < uSpotLightCount) {\n      vec3 lightPosition = (uViewMatrix * vec4(uSpotLightLocation[j], 1.0)).xyz;\n      vec3 lightVector = modelPosition - lightPosition;\n    \n      float lightDistance = length(lightVector);\n      float lightFalloff = 1.0 / (uConstantAttenuation + lightDistance * uLinearAttenuation + (lightDistance * lightDistance) * uQuadraticAttenuation);\n\n      vec3 lightDirection = (uViewMatrix * vec4(uSpotLightDirection[j], 0.0)).xyz;\n      float spotDot = dot(normalize(lightVector), normalize(lightDirection));\n      float spotFalloff;\n      if(spotDot < uSpotLightAngle[j]) {\n        spotFalloff = 0.0;\n      }\n      else {\n        spotFalloff = pow(spotDot, uSpotLightConc[j]);\n      }\n      lightFalloff *= spotFalloff;\n\n      vec3 lightColor = uSpotLightDiffuseColors[j];\n      vec3 specularColor = uSpotLightSpecularColors[j];\n     \n      LightResult result = _light(viewDirection, normal, lightVector, shininess, metallic);\n      \n      totalDiffuse += result.diffuse * lightColor * lightFalloff;\n      totalSpecular += result.specular * lightColor * specularColor * lightFalloff;\n    }\n  }\n\n  if( uUseImageLight ){\n    totalDiffuse += calculateImageDiffuse(normal, modelPosition, metallic);\n    totalSpecular += calculateImageSpecular(normal, modelPosition, shininess, metallic);\n  }\n\n  totalDiffuse *= diffuseFactor;\n  totalSpecular *= specularFactor;\n}\n";

  var normalVert = "IN vec3 aPosition;\nIN vec3 aNormal;\nIN vec2 aTexCoord;\nIN vec4 aVertexColor;\n\n#define HOOK_DEFINES\n\n#ifdef AUGMENTED_HOOK_getWorldInputs\nuniform mat4 uModelMatrix;\nuniform mat4 uViewMatrix;\nuniform mat3 uModelNormalMatrix;\nuniform mat3 uCameraNormalMatrix;\n#else\nuniform mat4 uModelViewMatrix;\nuniform mat3 uNormalMatrix;\n#endif\nuniform mat4 uProjectionMatrix;\n\nuniform vec4 uMaterialColor;\nuniform bool uUseVertexColor;\n\nOUT vec3 vVertexNormal;\nOUT highp vec2 vVertTexCoord;\nOUT vec4 vColor;\n\nstruct Vertex {\n  vec3 position;\n  vec3 normal;\n  vec2 texCoord;\n  vec4 color;\n};\n\nvoid main(void) {\n  HOOK_beforeVertex();\n\n  Vertex inputs;\n  inputs.position = aPosition;\n  inputs.normal = aNormal;\n  inputs.texCoord = aTexCoord;\n  inputs.color = (uUseVertexColor && aVertexColor.x >= 0.0) ? aVertexColor : uMaterialColor;\n#ifdef AUGMENTED_HOOK_getObjectInputs\n  inputs = HOOK_getObjectInputs(inputs);\n#endif\n\n#ifdef AUGMENTED_HOOK_getWorldInputs\n  inputs.position = (uModelMatrix * vec4(inputs.position, 1.)).xyz;\n  inputs.normal = uModelNormalMatrix * inputs.normal;\n  inputs = HOOK_getWorldInputs(inputs);\n#endif\n\n#ifdef AUGMENTED_HOOK_getWorldInputs\n  // Already multiplied by the model matrix, just apply view\n  inputs.position = (uViewMatrix * vec4(inputs.position, 1.)).xyz;\n  inputs.normal = uCameraNormalMatrix * inputs.normal;\n#else\n  // Apply both at once\n  inputs.position = (uModelViewMatrix * vec4(inputs.position, 1.)).xyz;\n  inputs.normal = uNormalMatrix * inputs.normal;\n#endif\n#ifdef AUGMENTED_HOOK_getCameraInputs\n  inputs = HOOK_getCameraInputs(inputs);\n#endif\n\n  // Pass varyings to fragment shader\n  vVertTexCoord = inputs.texCoord;\n  vVertexNormal = normalize(inputs.normal);\n  vColor = inputs.color;\n\n  gl_Position = uProjectionMatrix * vec4(inputs.position, 1.);\n\n  HOOK_afterVertex();\n}\n";

  var normalFrag = "IN vec3 vVertexNormal;\nvoid main(void) {\n  HOOK_beforeFragment();\n  OUT_COLOR = HOOK_getFinalColor(vec4(vVertexNormal, 1.0));\n  HOOK_afterFragment();\n}\n";

  var basicFrag = "IN vec4 vColor;\nvoid main(void) {\n  HOOK_beforeFragment();\n  OUT_COLOR = HOOK_getFinalColor(vColor);\n  OUT_COLOR.rgb *= OUT_COLOR.a; // Premultiply alpha before rendering\n  HOOK_afterFragment();\n}\n";

  var lightVert = "// include lighting.glgl\n\nIN vec3 aPosition;\nIN vec3 aNormal;\nIN vec2 aTexCoord;\nIN vec4 aVertexColor;\n\nuniform mat4 uModelViewMatrix;\nuniform mat4 uProjectionMatrix;\nuniform mat3 uNormalMatrix;\n\nuniform bool uUseVertexColor;\nuniform vec4 uMaterialColor;\n\nOUT highp vec2 vVertTexCoord;\nOUT vec3 vDiffuseColor;\nOUT vec3 vSpecularColor;\nOUT vec4 vColor;\n\nvoid main(void) {\n\n  vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0);\n  gl_Position = uProjectionMatrix * viewModelPosition;\n\n  vec3 vertexNormal = normalize(uNormalMatrix * aNormal);\n  vVertTexCoord = aTexCoord;\n\n  totalLight(viewModelPosition.xyz, vertexNormal, vDiffuseColor, vSpecularColor);\n\n  for (int i = 0; i < 8; i++) {\n    if (i < uAmbientLightCount) {\n      vDiffuseColor += uAmbientColor[i];\n    }\n  }\n  \n  vColor = ((uUseVertexColor && aVertexColor.x >= 0.0) ? aVertexColor : uMaterialColor);\n}\n";

  var lightTextureFrag = "uniform vec4 uTint;\nuniform sampler2D uSampler;\nuniform bool isTexture;\nuniform bool uEmissive;\n\nIN highp vec2 vVertTexCoord;\nIN vec3 vDiffuseColor;\nIN vec3 vSpecularColor;\nIN vec4 vColor;\n\nvoid main(void) {\n  if(uEmissive && !isTexture) {\n    OUT_COLOR = vColor;\n  }\n  else {\n    vec4 baseColor = isTexture\n      // Textures come in with premultiplied alpha. To apply tint and still have\n      // premultiplied alpha output, we need to multiply the RGB channels by the\n      // tint RGB, and all channels by the tint alpha.\n      ? TEXTURE(uSampler, vVertTexCoord) * vec4(uTint.rgb/255., 1.) * (uTint.a/255.)\n      // Colors come in with unmultiplied alpha, so we need to multiply the RGB\n      // channels by alpha to convert it to premultiplied alpha.\n      : vec4(vColor.rgb * vColor.a, vColor.a);\n    OUT_COLOR = vec4(baseColor.rgb * vDiffuseColor + vSpecularColor, baseColor.a);\n  }\n}\n";

  var phongVert = "precision highp int;\n\n#define HOOK_DEFINES\n\nIN vec3 aPosition;\nIN vec3 aNormal;\nIN vec2 aTexCoord;\nIN vec4 aVertexColor;\n\n#ifdef AUGMENTED_HOOK_getWorldInputs\nuniform mat4 uModelMatrix;\nuniform mat4 uViewMatrix;\nuniform mat3 uModelNormalMatrix;\nuniform mat3 uCameraNormalMatrix;\n#else\nuniform mat4 uModelViewMatrix;\nuniform mat3 uNormalMatrix;\n#endif\nuniform mat4 uProjectionMatrix;\n\nuniform bool uUseVertexColor;\nuniform vec4 uMaterialColor;\n\nOUT vec3 vNormal;\nOUT vec2 vTexCoord;\nOUT vec3 vViewPosition;\nOUT vec3 vAmbientColor;\nOUT vec4 vColor;\n\nstruct Vertex {\n  vec3 position;\n  vec3 normal;\n  vec2 texCoord;\n  vec4 color;\n};\n\nvoid main(void) {\n  HOOK_beforeVertex();\n\n  Vertex inputs;\n  inputs.position = aPosition;\n  inputs.normal = aNormal;\n  inputs.texCoord = aTexCoord;\n  inputs.color = (uUseVertexColor && aVertexColor.x >= 0.0) ? aVertexColor : uMaterialColor;\n#ifdef AUGMENTED_HOOK_getObjectInputs\n  inputs = HOOK_getObjectInputs(inputs);\n#endif\n\n#ifdef AUGMENTED_HOOK_getWorldInputs\n  inputs.position = (uModelMatrix * vec4(inputs.position, 1.)).xyz;\n  inputs.normal = uModelNormalMatrix * inputs.normal;\n  inputs = HOOK_getWorldInputs(inputs);\n#endif\n\n#ifdef AUGMENTED_HOOK_getWorldInputs\n  // Already multiplied by the model matrix, just apply view\n  inputs.position = (uViewMatrix * vec4(inputs.position, 1.)).xyz;\n  inputs.normal = uCameraNormalMatrix * inputs.normal;\n#else\n  // Apply both at once\n  inputs.position = (uModelViewMatrix * vec4(inputs.position, 1.)).xyz;\n  inputs.normal = uNormalMatrix * inputs.normal;\n#endif\n#ifdef AUGMENTED_HOOK_getCameraInputs\n  inputs = HOOK_getCameraInputs(inputs);\n#endif\n\n  // Pass varyings to fragment shader\n  vViewPosition = inputs.position;\n  vTexCoord = inputs.texCoord;\n  vNormal = inputs.normal;\n  vColor = inputs.color;\n\n  gl_Position = uProjectionMatrix * vec4(inputs.position, 1.);\n  HOOK_afterVertex();\n}\n";

  var phongFrag = "// include lighting.glsl\nprecision highp int;\n\nuniform bool uHasSetAmbient;\nuniform vec3 uAmbientColor;\nuniform vec4 uSpecularMatColor;\nuniform vec4 uAmbientMatColor;\nuniform vec4 uEmissiveMatColor;\n\nuniform vec4 uTint;\nuniform sampler2D uSampler;\nuniform bool isTexture;\n\nIN vec3 vNormal;\nIN vec2 vTexCoord;\nIN vec3 vViewPosition;\nIN vec4 vColor;\n\nstruct ColorComponents {\n  vec3 baseColor;\n  float opacity;\n  vec3 ambientColor;\n  vec3 specularColor;\n  vec3 diffuse;\n  vec3 ambient;\n  vec3 specular;\n  vec3 emissive;\n};\n\nstruct Inputs {\n  vec3 normal;\n  vec2 texCoord;\n  vec3 ambientLight;\n  vec3 ambientMaterial;\n  vec3 specularMaterial;\n  vec3 emissiveMaterial;\n  vec4 color;\n  float shininess;\n  float metalness;\n};\n\nvoid main(void) {\n  HOOK_beforeFragment();\n\n  Inputs inputs;\n  inputs.normal = normalize(vNormal);\n  inputs.texCoord = vTexCoord;\n  inputs.ambientLight = uAmbientColor;\n  inputs.color = isTexture\n      ? TEXTURE(uSampler, vTexCoord) * (vec4(uTint.rgb/255., 1.) * uTint.a/255.)\n      : vColor;\n  if (isTexture && inputs.color.a > 0.0) {\n    // Textures come in with premultiplied alpha. Temporarily unpremultiply it\n    // so hooks users don't have to think about premultiplied alpha.\n    inputs.color.rgb /= inputs.color.a;\n  }\n  inputs.shininess = uShininess;\n  inputs.metalness = uMetallic;\n  inputs.ambientMaterial = uHasSetAmbient ? uAmbientMatColor.rgb : inputs.color.rgb;\n  inputs.specularMaterial = uSpecularMatColor.rgb;\n  inputs.emissiveMaterial = uEmissiveMatColor.rgb;\n  inputs = HOOK_getPixelInputs(inputs);\n\n  vec3 diffuse;\n  vec3 specular;\n  totalLight(vViewPosition, inputs.normal, inputs.shininess, inputs.metalness, diffuse, specular);\n\n  // Calculating final color as result of all lights (plus emissive term).\n\n  vec4 baseColor = inputs.color;\n  ColorComponents c;\n  c.opacity = baseColor.a;\n  c.baseColor = baseColor.rgb;\n  c.ambientColor = inputs.ambientMaterial;\n  c.specularColor = inputs.specularMaterial;\n  c.diffuse = diffuse;\n  c.ambient = inputs.ambientLight;\n  c.specular = specular;\n  c.emissive = inputs.emissiveMaterial;\n  OUT_COLOR = HOOK_getFinalColor(HOOK_combineColors(c));\n  OUT_COLOR.rgb *= OUT_COLOR.a; // Premultiply alpha before rendering\n  HOOK_afterFragment();\n}\n";

  var fontVert = "IN vec3 aPosition;\nIN vec2 aTexCoord;\nuniform mat4 uModelViewMatrix;\nuniform mat4 uProjectionMatrix;\n\nuniform vec4 uGlyphRect;\nuniform float uGlyphOffset;\n\nOUT vec2 vTexCoord;\n\nvoid main() {\n  vec4 positionVec4 = vec4(aPosition, 1.0);\n\n  // scale by the size of the glyph's rectangle\n  positionVec4.xy *= uGlyphRect.zw - uGlyphRect.xy;\n\n  // Expand glyph bounding boxes by 1px on each side to give a bit of room\n  // for antialiasing\n  vec3 newOrigin = (uModelViewMatrix * vec4(0., 0., 0., 1.)).xyz;\n  vec3 newDX = (uModelViewMatrix * vec4(1., 0., 0., 1.)).xyz;\n  vec3 newDY = (uModelViewMatrix * vec4(0., 1., 0., 1.)).xyz;\n  vec2 pixelScale = vec2(\n    1. / length(newOrigin - newDX),\n    1. / length(newOrigin - newDY)\n  );\n  vec2 offset = pixelScale * normalize(aTexCoord - vec2(0.5, 0.5));\n  vec2 textureOffset = offset * (1. / vec2(\n    uGlyphRect.z - uGlyphRect.x,\n    uGlyphRect.w - uGlyphRect.y\n  ));\n\n  // move to the corner of the glyph\n  positionVec4.xy += uGlyphRect.xy;\n\n  // move to the letter's line offset\n  positionVec4.x += uGlyphOffset;\n\n  positionVec4.xy += offset;\n  \n  gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;\n  vTexCoord = aTexCoord + textureOffset;\n}\n";

  var fontFrag = "#ifndef WEBGL2\n#extension GL_OES_standard_derivatives : enable\n#endif\n\n#if 0\n  // simulate integer math using floats\n\t#define int float\n\t#define ivec2 vec2\n\t#define INT(x) float(x)\n\n\tint ifloor(float v) { return floor(v); }\n\tivec2 ifloor(vec2 v) { return floor(v); }\n\n#else\n  // use native integer math\n\tprecision highp int;\n\t#define INT(x) x\n\n\tint ifloor(float v) { return int(v); }\n\tint ifloor(int v) { return v; }\n\tivec2 ifloor(vec2 v) { return ivec2(v); }\n\n#endif\n\nuniform sampler2D uSamplerStrokes;\nuniform sampler2D uSamplerRowStrokes;\nuniform sampler2D uSamplerRows;\nuniform sampler2D uSamplerColStrokes;\nuniform sampler2D uSamplerCols;\n\nuniform ivec2 uStrokeImageSize;\nuniform ivec2 uCellsImageSize;\nuniform ivec2 uGridImageSize;\n\nuniform ivec2 uGridOffset;\nuniform ivec2 uGridSize;\nuniform vec4 uMaterialColor;\n\nIN vec2 vTexCoord;\n\n// some helper functions\nint ROUND(float v) { return ifloor(v + 0.5); }\nivec2 ROUND(vec2 v) { return ifloor(v + 0.5); }\nfloat saturate(float v) { return clamp(v, 0.0, 1.0); }\nvec2 saturate(vec2 v) { return clamp(v, 0.0, 1.0); }\n\nint mul(float v1, int v2) {\n  return ifloor(v1 * float(v2));\n}\n\nivec2 mul(vec2 v1, ivec2 v2) {\n  return ifloor(v1 * vec2(v2) + 0.5);\n}\n\n// unpack a 16-bit integer from a float vec2\nint getInt16(vec2 v) {\n  ivec2 iv = ROUND(v * 255.0);\n  return iv.x * INT(128) + iv.y;\n}\n\nvec2 pixelScale;\nvec2 coverage = vec2(0.0);\nvec2 weight = vec2(0.5);\nconst float minDistance = 1.0/8192.0;\nconst float hardness = 1.05; // amount of antialias\n\n// the maximum number of curves in a glyph\nconst int N = INT(250);\n\n// retrieves an indexed pixel from a sampler\nvec4 getTexel(sampler2D sampler, int pos, ivec2 size) {\n  int width = size.x;\n  int y = ifloor(pos / width);\n  int x = pos - y * width;  // pos % width\n\n  return TEXTURE(sampler, (vec2(x, y) + 0.5) / vec2(size));\n}\n\nvoid calulateCrossings(vec2 p0, vec2 p1, vec2 p2, out vec2 C1, out vec2 C2) {\n\n  // get the coefficients of the quadratic in t\n  vec2 a = p0 - p1 * 2.0 + p2;\n  vec2 b = p0 - p1;\n  vec2 c = p0 - vTexCoord;\n\n  // found out which values of 't' it crosses the axes\n  vec2 surd = sqrt(max(vec2(0.0), b * b - a * c));\n  vec2 t1 = ((b - surd) / a).yx;\n  vec2 t2 = ((b + surd) / a).yx;\n\n  // approximate straight lines to avoid rounding errors\n  if (abs(a.y) < 0.001)\n    t1.x = t2.x = c.y / (2.0 * b.y);\n\n  if (abs(a.x) < 0.001)\n    t1.y = t2.y = c.x / (2.0 * b.x);\n\n  // plug into quadratic formula to find the corrdinates of the crossings\n  C1 = ((a * t1 - b * 2.0) * t1 + c) * pixelScale;\n  C2 = ((a * t2 - b * 2.0) * t2 + c) * pixelScale;\n}\n\nvoid coverageX(vec2 p0, vec2 p1, vec2 p2) {\n\n  vec2 C1, C2;\n  calulateCrossings(p0, p1, p2, C1, C2);\n\n  // determine on which side of the x-axis the points lie\n  bool y0 = p0.y > vTexCoord.y;\n  bool y1 = p1.y > vTexCoord.y;\n  bool y2 = p2.y > vTexCoord.y;\n\n  // could web be under the curve (after t1)?\n  if (y1 ? !y2 : y0) {\n    // add the coverage for t1\n    coverage.x += saturate(C1.x + 0.5);\n    // calculate the anti-aliasing for t1\n    weight.x = min(weight.x, abs(C1.x));\n  }\n\n  // are we outside the curve (after t2)?\n  if (y1 ? !y0 : y2) {\n    // subtract the coverage for t2\n    coverage.x -= saturate(C2.x + 0.5);\n    // calculate the anti-aliasing for t2\n    weight.x = min(weight.x, abs(C2.x));\n  }\n}\n\n// this is essentially the same as coverageX, but with the axes swapped\nvoid coverageY(vec2 p0, vec2 p1, vec2 p2) {\n\n  vec2 C1, C2;\n  calulateCrossings(p0, p1, p2, C1, C2);\n\n  bool x0 = p0.x > vTexCoord.x;\n  bool x1 = p1.x > vTexCoord.x;\n  bool x2 = p2.x > vTexCoord.x;\n\n  if (x1 ? !x2 : x0) {\n    coverage.y -= saturate(C1.y + 0.5);\n    weight.y = min(weight.y, abs(C1.y));\n  }\n\n  if (x1 ? !x0 : x2) {\n    coverage.y += saturate(C2.y + 0.5);\n    weight.y = min(weight.y, abs(C2.y));\n  }\n}\n\nvoid main() {\n\n  // calculate the pixel scale based on screen-coordinates\n  pixelScale = hardness / fwidth(vTexCoord);\n\n  // which grid cell is this pixel in?\n  ivec2 gridCoord = ifloor(vTexCoord * vec2(uGridSize));\n\n  // intersect curves in this row\n  {\n    // the index into the row info bitmap\n    int rowIndex = gridCoord.y + uGridOffset.y;\n    // fetch the info texel\n    vec4 rowInfo = getTexel(uSamplerRows, rowIndex, uGridImageSize);\n    // unpack the rowInfo\n    int rowStrokeIndex = getInt16(rowInfo.xy);\n    int rowStrokeCount = getInt16(rowInfo.zw);\n\n    for (int iRowStroke = INT(0); iRowStroke < N; iRowStroke++) {\n      if (iRowStroke >= rowStrokeCount)\n        break;\n\n      // each stroke is made up of 3 points: the start and control point\n      // and the start of the next curve.\n      // fetch the indices of this pair of strokes:\n      vec4 strokeIndices = getTexel(uSamplerRowStrokes, rowStrokeIndex++, uCellsImageSize);\n\n      // unpack the stroke index\n      int strokePos = getInt16(strokeIndices.xy);\n\n      // fetch the two strokes\n      vec4 stroke0 = getTexel(uSamplerStrokes, strokePos + INT(0), uStrokeImageSize);\n      vec4 stroke1 = getTexel(uSamplerStrokes, strokePos + INT(1), uStrokeImageSize);\n\n      // calculate the coverage\n      coverageX(stroke0.xy, stroke0.zw, stroke1.xy);\n    }\n  }\n\n  // intersect curves in this column\n  {\n    int colIndex = gridCoord.x + uGridOffset.x;\n    vec4 colInfo = getTexel(uSamplerCols, colIndex, uGridImageSize);\n    int colStrokeIndex = getInt16(colInfo.xy);\n    int colStrokeCount = getInt16(colInfo.zw);\n    \n    for (int iColStroke = INT(0); iColStroke < N; iColStroke++) {\n      if (iColStroke >= colStrokeCount)\n        break;\n\n      vec4 strokeIndices = getTexel(uSamplerColStrokes, colStrokeIndex++, uCellsImageSize);\n\n      int strokePos = getInt16(strokeIndices.xy);\n      vec4 stroke0 = getTexel(uSamplerStrokes, strokePos + INT(0), uStrokeImageSize);\n      vec4 stroke1 = getTexel(uSamplerStrokes, strokePos + INT(1), uStrokeImageSize);\n      coverageY(stroke0.xy, stroke0.zw, stroke1.xy);\n    }\n  }\n\n  weight = saturate(1.0 - weight * 2.0);\n  float distance = max(weight.x + weight.y, minDistance); // manhattan approx.\n  float antialias = abs(dot(coverage, weight) / distance);\n  float cover = min(abs(coverage.x), abs(coverage.y));\n  OUT_COLOR = vec4(uMaterialColor.rgb, 1.) * uMaterialColor.a;\n  OUT_COLOR *= saturate(max(antialias, cover));\n}\n";

  var lineVert = "/*\n  Part of the Processing project - http://processing.org\n  Copyright (c) 2012-15 The Processing Foundation\n  Copyright (c) 2004-12 Ben Fry and Casey Reas\n  Copyright (c) 2001-04 Massachusetts Institute of Technology\n  This library is free software; you can redistribute it and/or\n  modify it under the terms of the GNU Lesser General Public\n  License as published by the Free Software Foundation, version 2.1.\n  This library is distributed in the hope that it will be useful,\n  but WITHOUT ANY WARRANTY; without even the implied warranty of\n  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n  Lesser General Public License for more details.\n  You should have received a copy of the GNU Lesser General\n  Public License along with this library; if not, write to the\n  Free Software Foundation, Inc., 59 Temple Place, Suite 330,\n  Boston, MA  02111-1307  USA\n*/\n\n#define PROCESSING_LINE_SHADER\n\n#define HOOK_DEFINES\n\nprecision highp int;\nprecision highp float;\n\n#ifdef AUGMENTED_HOOK_getWorldInputs\nuniform mat4 uModelMatrix;\nuniform mat4 uViewMatrix;\n#else\nuniform mat4 uModelViewMatrix;\n#endif\n\nuniform mat4 uProjectionMatrix;\nuniform float uStrokeWeight;\n\nuniform bool uUseLineColor;\nuniform bool uSimpleLines;\nuniform vec4 uMaterialColor;\n\nuniform vec4 uViewport;\nuniform int uPerspective;\nuniform int uStrokeJoin;\n\nIN vec3 aPosition;\nIN vec3 aTangentIn;\nIN vec3 aTangentOut;\nIN float aSide;\nIN vec4 aVertexColor;\n\nOUT vec4 vColor;\nOUT vec2 vTangent;\nOUT vec2 vCenter;\nOUT vec2 vPosition;\nOUT float vMaxDist;\nOUT float vCap;\nOUT float vJoin;\nOUT float vStrokeWeight;\n\nvec2 lineIntersection(vec2 aPoint, vec2 aDir, vec2 bPoint, vec2 bDir) {\n  // Rotate and translate so a starts at the origin and goes out to the right\n  bPoint -= aPoint;\n  vec2 rotatedBFrom = vec2(\n    bPoint.x*aDir.x + bPoint.y*aDir.y,\n    bPoint.y*aDir.x - bPoint.x*aDir.y\n  );\n  vec2 bTo = bPoint + bDir;\n  vec2 rotatedBTo = vec2(\n    bTo.x*aDir.x + bTo.y*aDir.y,\n    bTo.y*aDir.x - bTo.x*aDir.y\n  );\n  float intersectionDistance =\n    rotatedBTo.x + (rotatedBFrom.x - rotatedBTo.x) * rotatedBTo.y /\n    (rotatedBTo.y - rotatedBFrom.y);\n  return aPoint + aDir * intersectionDistance;\n}\n\nstruct StrokeVertex {\n  vec3 position;\n  vec3 tangentIn;\n  vec3 tangentOut;\n  vec4 color;\n  float weight;\n};\n\nvoid main() {\n  HOOK_beforeVertex();\n\n  if (!uSimpleLines) {\n      // Caps have one of either the in or out tangent set to 0\n      vCap = (aTangentIn == vec3(0.)) != (aTangentOut == vec3(0.)) ? 1. : 0.;\n\n      // Joins have two unique, defined tangents\n      vJoin = (\n          aTangentIn != vec3(0.) &&\n          aTangentOut != vec3(0.) &&\n          aTangentIn != aTangentOut\n      ) ? 1. : 0.;\n  }\n\n  StrokeVertex inputs;\n  inputs.position = aPosition.xyz;\n  inputs.color = uUseLineColor ? aVertexColor : uMaterialColor;\n  inputs.weight = uStrokeWeight;\n  inputs.tangentIn = aTangentIn;\n  inputs.tangentOut = aTangentOut;\n\n#ifdef AUGMENTED_HOOK_getObjectInputs\n  inputs = HOOK_getObjectInputs(inputs);\n#endif\n\n#ifdef AUGMENTED_HOOK_getWorldInputs\n  inputs.position = (uModelMatrix * vec4(inputs.position, 1.)).xyz;\n  inputs.tangentIn = (uModelMatrix * vec4(aTangentIn, 0.)).xyz;\n  inputs.tangentOut = (uModelMatrix * vec4(aTangentOut, 0.)).xyz;\n  inputs = HOOK_getWorldInputs(inputs);\n#endif\n\n#ifdef AUGMENTED_HOOK_getWorldInputs\n  // Already multiplied by the model matrix, just apply view\n  inputs.position = (uViewMatrix * vec4(inputs.position, 1.)).xyz;\n  inputs.tangentIn = (uViewMatrix * vec4(aTangentIn, 0.)).xyz;\n  inputs.tangentOut = (uViewMatrix * vec4(aTangentOut, 0.)).xyz;\n#else\n  // Apply both at once\n  inputs.position = (uModelViewMatrix * vec4(inputs.position, 1.)).xyz;\n  inputs.tangentIn = (uModelViewMatrix * vec4(aTangentIn, 0.)).xyz;\n  inputs.tangentOut = (uModelViewMatrix * vec4(aTangentOut, 0.)).xyz;\n#endif\n#ifdef AUGMENTED_HOOK_getCameraInputs\n  inputs = HOOK_getCameraInputs(inputs);\n#endif\n\n  vec4 posp = vec4(inputs.position, 1.);\n  vec4 posqIn = vec4(inputs.position + inputs.tangentIn, 1.);\n  vec4 posqOut = vec4(inputs.position + inputs.tangentOut, 1.);\n  vStrokeWeight = inputs.weight;\n\n  float facingCamera = pow(\n    // The word space tangent's z value is 0 if it's facing the camera\n    abs(normalize(posqIn-posp).z),\n\n    // Using pow() here to ramp `facingCamera` up from 0 to 1 really quickly\n    // so most lines get scaled and don't get clipped\n    0.25\n  );\n\n  // Moving vertices slightly toward the camera\n  // to avoid depth-fighting with the fill triangles.\n  // A mix of scaling and offsetting is used based on distance\n  // Discussion here:\n  // https://github.com/processing/p5.js/issues/7200 \n\n  // using a scale <1 moves the lines towards nearby camera\n  // in order to prevent popping effects due to half of\n  // the line disappearing behind the geometry faces.\n  float zDistance = -posp.z; \n  float distanceFactor = smoothstep(0.0, 800.0, zDistance); \n  \n  // Discussed here:\n  // http://www.opengl.org/discussion_boards/ubbthreads.php?ubb=showflat&Number=252848  \n  float scale = mix(1., 0.995, facingCamera);\n  float dynamicScale = mix(scale, 1.0, distanceFactor); // Closer = more scale, farther = less\n\n  posp.xyz = posp.xyz * dynamicScale;\n  posqIn.xyz = posqIn.xyz * dynamicScale;\n  posqOut.xyz = posqOut.xyz * dynamicScale;\n\n  // Moving vertices slightly toward camera when far away \n  // https://github.com/processing/p5.js/issues/6956 \n  float zOffset = mix(0., -1., facingCamera);\n  float dynamicZAdjustment = mix(0.0, zOffset, distanceFactor); // Closer = less zAdjustment, farther = more\n\n  posp.z -= dynamicZAdjustment;\n  posqIn.z -= dynamicZAdjustment;\n  posqOut.z -= dynamicZAdjustment;\n  \n  vec4 p = uProjectionMatrix * posp;\n  vec4 qIn = uProjectionMatrix * posqIn;\n  vec4 qOut = uProjectionMatrix * posqOut;\n\n  // formula to convert from clip space (range -1..1) to screen space (range 0..[width or height])\n  // screen_p = (p.xy/p.w + <1,1>) * 0.5 * uViewport.zw\n\n  // prevent division by W by transforming the tangent formula (div by 0 causes\n  // the line to disappear, see https://github.com/processing/processing/issues/5183)\n  // t = screen_q - screen_p\n  //\n  // tangent is normalized and we don't care which aDirection it points to (+-)\n  // t = +- normalize( screen_q - screen_p )\n  // t = +- normalize( (q.xy/q.w+<1,1>)*0.5*uViewport.zw - (p.xy/p.w+<1,1>)*0.5*uViewport.zw )\n  //\n  // extract common factor, <1,1> - <1,1> cancels out\n  // t = +- normalize( (q.xy/q.w - p.xy/p.w) * 0.5 * uViewport.zw )\n  //\n  // convert to common divisor\n  // t = +- normalize( ((q.xy*p.w - p.xy*q.w) / (p.w*q.w)) * 0.5 * uViewport.zw )\n  //\n  // remove the common scalar divisor/factor, not needed due to normalize and +-\n  // (keep uViewport - can't remove because it has different components for x and y\n  //  and corrects for aspect ratio, see https://github.com/processing/processing/issues/5181)\n  // t = +- normalize( (q.xy*p.w - p.xy*q.w) * uViewport.zw )\n\n  vec2 tangentIn = normalize((qIn.xy*p.w - p.xy*qIn.w) * uViewport.zw);\n  vec2 tangentOut = normalize((qOut.xy*p.w - p.xy*qOut.w) * uViewport.zw);\n\n  vec2 curPerspScale;\n  if(uPerspective == 1) {\n    // Perspective ---\n    // convert from world to clip by multiplying with projection scaling factor\n    // to get the right thickness (see https://github.com/processing/processing/issues/5182)\n\n    // The y value of the projection matrix may be flipped if rendering to a Framebuffer.\n    // Multiplying again by its sign here negates the flip to get just the scale.\n    curPerspScale = (uProjectionMatrix * vec4(1, sign(uProjectionMatrix[1][1]), 0, 0)).xy;\n  } else {\n    // No Perspective ---\n    // multiply by W (to cancel out division by W later in the pipeline) and\n    // convert from screen to clip (derived from clip to screen above)\n    curPerspScale = p.w / (0.5 * uViewport.zw);\n  }\n\n  vec2 offset;\n  if (vJoin == 1. && !uSimpleLines) {\n    vTangent = normalize(tangentIn + tangentOut);\n    vec2 normalIn = vec2(-tangentIn.y, tangentIn.x);\n    vec2 normalOut = vec2(-tangentOut.y, tangentOut.x);\n    float side = sign(aSide);\n    float sideEnum = abs(aSide);\n\n    // We generate vertices for joins on either side of the centerline, but\n    // the \"elbow\" side is the only one needing a join. By not setting the\n    // offset for the other side, all its vertices will end up in the same\n    // spot and not render, effectively discarding it.\n    if (sign(dot(tangentOut, vec2(-tangentIn.y, tangentIn.x))) != side) {\n      // Side enums:\n      //   1: the side going into the join\n      //   2: the middle of the join\n      //   3: the side going out of the join\n      if (sideEnum == 2.) {\n        // Calculate the position + tangent on either side of the join, and\n        // find where the lines intersect to find the elbow of the join\n        vec2 c = (posp.xy/posp.w + vec2(1.,1.)) * 0.5 * uViewport.zw;\n        vec2 intersection = lineIntersection(\n          c + (side * normalIn * inputs.weight / 2.),\n          tangentIn,\n          c + (side * normalOut * inputs.weight / 2.),\n          tangentOut\n        );\n        offset = (intersection - c);\n\n        // When lines are thick and the angle of the join approaches 180, the\n        // elbow might be really far from the center. We'll apply a limit to\n        // the magnitude to avoid lines going across the whole screen when this\n        // happens.\n        float mag = length(offset);\n        float maxMag = 3. * inputs.weight;\n        if (mag > maxMag) {\n          offset *= maxMag / mag;\n        }\n      } else if (sideEnum == 1.) {\n        offset = side * normalIn * inputs.weight / 2.;\n      } else if (sideEnum == 3.) {\n        offset = side * normalOut * inputs.weight / 2.;\n      }\n    }\n    if (uStrokeJoin == STROKE_JOIN_BEVEL) {\n      vec2 avgNormal = vec2(-vTangent.y, vTangent.x);\n      vMaxDist = abs(dot(avgNormal, normalIn * inputs.weight / 2.));\n    } else {\n      vMaxDist = inputs.weight / 2.;\n    }\n  } else {\n    vec2 tangent = aTangentIn == vec3(0.) ? tangentOut : tangentIn;\n\n    vTangent = tangent;\n    vec2 normal = vec2(-tangent.y, tangent.x);\n\n    float normalOffset = sign(aSide);\n    // Caps will have side values of -2 or 2 on the edge of the cap that\n    // extends out from the line\n    float tangentOffset = abs(aSide) - 1.;\n    offset = (normal * normalOffset + tangent * tangentOffset) *\n      inputs.weight * 0.5;\n    vMaxDist = inputs.weight / 2.;\n  }\n\n  vCenter = p.xy;\n  vPosition = vCenter + offset;\n  vColor = inputs.color;\n\n  gl_Position.xy = p.xy + offset.xy * curPerspScale;\n  gl_Position.zw = p.zw;\n  \n  HOOK_afterVertex();\n}\n";

  var lineFrag = "precision highp int;\nprecision highp float;\n\nuniform vec4 uMaterialColor;\nuniform int uStrokeCap;\nuniform int uStrokeJoin;\n\nIN vec4 vColor;\nIN vec2 vTangent;\nIN vec2 vCenter;\nIN vec2 vPosition;\nIN float vStrokeWeight;\nIN float vMaxDist;\nIN float vCap;\nIN float vJoin;\n\nfloat distSquared(vec2 a, vec2 b) {\n  vec2 aToB = b - a;\n  return dot(aToB, aToB);\n}\n\nstruct Inputs {\n  vec4 color;\n  vec2 tangent;\n  vec2 center;\n  vec2 position;\n  float strokeWeight;\n};\n\nvoid main() {\n  HOOK_beforeFragment();\n\n  Inputs inputs;\n  inputs.color = vColor;\n  inputs.tangent = vTangent;\n  inputs.center = vCenter;\n  inputs.position = vPosition;\n  inputs.strokeWeight = vStrokeWeight;\n  inputs = HOOK_getPixelInputs(inputs);\n\n  if (vCap > 0.) {\n    if (\n      uStrokeCap == STROKE_CAP_ROUND &&\n      HOOK_shouldDiscard(distSquared(inputs.position, inputs.center) > inputs.strokeWeight * inputs.strokeWeight * 0.25)\n    ) {\n      discard;\n    } else if (\n      uStrokeCap == STROKE_CAP_SQUARE &&\n      HOOK_shouldDiscard(dot(inputs.position - inputs.center, inputs.tangent) > 0.)\n    ) {\n      discard;\n    // Use full area for PROJECT\n    } else if (HOOK_shouldDiscard(false)) {\n      discard;\n    }\n  } else if (vJoin > 0.) {\n    if (\n      uStrokeJoin == STROKE_JOIN_ROUND &&\n      HOOK_shouldDiscard(distSquared(inputs.position, inputs.center) > inputs.strokeWeight * inputs.strokeWeight * 0.25)\n    ) {\n      discard;\n    } else if (uStrokeJoin == STROKE_JOIN_BEVEL) {\n      vec2 normal = vec2(-inputs.tangent.y, inputs.tangent.x);\n      if (HOOK_shouldDiscard(abs(dot(inputs.position - inputs.center, normal)) > vMaxDist)) {\n        discard;\n      }\n    // Use full area for MITER\n    } else if (HOOK_shouldDiscard(false)) {\n      discard;\n    }\n  }\n  OUT_COLOR = HOOK_getFinalColor(vec4(inputs.color.rgb, 1.) * inputs.color.a);\n  HOOK_afterFragment();\n}\n";

  var imageLightVert = "precision highp float;\nIN vec3 aPosition;\nIN vec3 aNormal;\nIN vec2 aTexCoord;\n\nOUT vec3 localPos;\nOUT vec3 vWorldNormal;\nOUT vec3 vWorldPosition;\nOUT vec2 vTexCoord;\n\nuniform mat4 uModelViewMatrix;\nuniform mat4 uProjectionMatrix;\nuniform mat3 uNormalMatrix;\n\nvoid main() {\n  // Multiply the position by the matrix.\n  vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0);\n  gl_Position = uProjectionMatrix * viewModelPosition;  \n  \n  // orient the normals and pass to the fragment shader\n  vWorldNormal = uNormalMatrix * aNormal;\n  \n  // send the view position to the fragment shader\n  vWorldPosition = (uModelViewMatrix * vec4(aPosition, 1.0)).xyz;\n  \n  localPos = vWorldPosition;\n  vTexCoord = aTexCoord;\n}\n\n\n/*\nin the vertex shader we'll compute the world position and world oriented normal of the vertices and pass those to the fragment shader as varyings.\n*/\n";

  var imageLightDiffusedFrag = "precision highp float;\nIN vec3 localPos;\n\n// the HDR cubemap converted (can be from an equirectangular environment map.)\nuniform sampler2D environmentMap;\nIN vec2 vTexCoord;\n\nconst float PI = 3.14159265359;\n\nvec2 nTOE( vec3 v ){\n  // x = r sin(phi) cos(theta)   \n  // y = r cos(phi)  \n  // z = r sin(phi) sin(theta)\n  float phi = acos( v.y );\n  // if phi is 0, then there are no x, z components\n  float theta = 0.0;\n  // else \n  theta = acos(v.x / sin(phi));\n  float sinTheta = v.z / sin(phi);\n  if (sinTheta < 0.0) {\n    // Turn it into -theta, but in the 0-2PI range\n    theta = 2.0 * PI - theta;\n  }\n  theta = theta / (2.0 * 3.14159);\n  phi = phi / 3.14159 ;\n  \n  vec2 angles = vec2( phi, theta );\n  return angles;\n}\n\nfloat random(vec2 p) {\n  vec3 p3  = fract(vec3(p.xyx) * .1031);\n  p3 += dot(p3, p3.yzx + 33.33);\n  return fract((p3.x + p3.y) * p3.z);\n}\n\nvoid main()\n{   \t \n\t// the sample direction equals the hemisphere's orientation\n  float phi = vTexCoord.x * 2.0 * PI;\n  float theta = vTexCoord.y * PI;\n  float x = sin(theta) * cos(phi);\n  float y = sin(theta) * sin(phi);\n  float z = cos(theta);\n  vec3 normal = vec3( x, y, z);\n\n\t// Discretely sampling the hemisphere given the integral's\n  // spherical coordinates translates to the following fragment code:\n\tvec3 irradiance = vec3(0.0);  \n\tvec3 up\t= vec3(0.0, 1.0, 0.0);\n\tvec3 right = normalize(cross(up, normal));\n\tup = normalize(cross(normal, right));\n\n\t//  We specify a fixed sampleDelta delta value to traverse\n  // the hemisphere; decreasing or increasing the sample delta\n  // will increase or decrease the accuracy respectively.\n\tconst float sampleDelta = 0.100;\n\tfloat nrSamples = 0.0;\n  float randomOffset = random(gl_FragCoord.xy) * sampleDelta;\n\tfor(float rawPhi = 0.0; rawPhi < 2.0 * PI; rawPhi += sampleDelta)\n\t{\n    float phi = rawPhi + randomOffset;\n    for(float rawTheta = 0.0; rawTheta < ( 0.5 ) * PI; rawTheta += sampleDelta)\n    {\n      float theta = rawTheta + randomOffset;\n      // spherical to cartesian (in tangent space) // tangent space to world // add each sample result to irradiance\n      float x = sin(theta) * cos(phi);\n      float y = sin(theta) * sin(phi);\n      float z = cos(theta);\n      vec3 tangentSample = vec3( x, y, z);\n      \n      vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * normal;\n        irradiance += (TEXTURE(environmentMap, nTOE(sampleVec)).xyz) * cos(theta) * sin(theta);\n      nrSamples++;\n    }\n\t}\n\t// divide by the total number of samples taken, giving us the average sampled irradiance.\n\tirradiance = PI * irradiance * (1.0 / float(nrSamples )) ;\n  \n \n\tOUT_COLOR = vec4(irradiance, 1.0);\n}";

  var imageLightSpecularFrag = "precision highp float;\r\nIN vec3 localPos;\r\nIN vec2 vTexCoord;\r\n\r\n// our texture\r\nuniform sampler2D environmentMap;\r\nuniform float roughness;\r\n\r\nconst float PI = 3.14159265359;\r\n\r\nfloat VanDerCorput(int bits);\r\nvec2 HammersleyNoBitOps(int i, int N);\r\nvec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness);\r\n\r\n\r\nvec2 nTOE( vec3 v ){\r\n  // x = r sin(phi) cos(theta)   \r\n  // y = r cos(phi)  \r\n  // z = r sin(phi) sin(theta)\r\n  float phi = acos( v.y );\r\n  // if phi is 0, then there are no x, z components\r\n  float theta = 0.0;\r\n  // else \r\n  theta = acos(v.x / sin(phi));\r\n  float sinTheta = v.z / sin(phi);\r\n  if (sinTheta < 0.0) {\r\n    // Turn it into -theta, but in the 0-2PI range\r\n    theta = 2.0 * PI - theta;\r\n  }\r\n  theta = theta / (2.0 * 3.14159);\r\n  phi = phi / 3.14159 ;\r\n  \r\n  vec2 angles = vec2( phi, theta );\r\n  return angles;\r\n}\r\n\r\n\r\nvoid main(){\r\n  const int SAMPLE_COUNT = 400; // 4096\r\n  int lowRoughnessLimit = int(pow(2.0,(roughness+0.1)*20.0));\r\n  float totalWeight = 0.0;\r\n  vec3 prefilteredColor = vec3(0.0);\r\n  float phi = vTexCoord.x * 2.0 * PI;\r\n  float theta = vTexCoord.y * PI;\r\n  float x = sin(theta) * cos(phi);\r\n  float y = sin(theta) * sin(phi);\r\n  float z = cos(theta);\r\n  vec3 N = vec3(x,y,z);\r\n  vec3 V = N;\r\n  for (int i = 0; i < SAMPLE_COUNT; ++i)\r\n  {\r\n    // break at smaller sample numbers for low roughness levels\r\n    if(i == lowRoughnessLimit)\r\n    {\r\n      break;\r\n    }\r\n    vec2 Xi = HammersleyNoBitOps(i, SAMPLE_COUNT);\r\n    vec3 H = ImportanceSampleGGX(Xi, N, roughness);\r\n    vec3 L = normalize(2.0 * dot(V, H) * H - V);\r\n\r\n    float NdotL = max(dot(N, L), 0.0);\r\n    if (NdotL > 0.0)\r\n    {\r\n      prefilteredColor += TEXTURE(environmentMap, nTOE(L)).xyz * NdotL;\r\n      totalWeight += NdotL;\r\n    }\r\n  }\r\n  prefilteredColor = prefilteredColor / totalWeight;\r\n\r\n OUT_COLOR  = vec4(prefilteredColor, 1.0);\r\n}\r\n\r\nvec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness){\r\n  float a = roughness * roughness;\r\n\r\n  float phi = 2.0 * PI * Xi.x;\r\n  float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a * a - 1.0) * Xi.y));\r\n  float sinTheta = sqrt(1.0 - cosTheta * cosTheta);\r\n  // from spherical coordinates to cartesian coordinates\r\n  vec3 H;\r\n  H.x = cos(phi) * sinTheta;\r\n  H.y = sin(phi) * sinTheta;\r\n  H.z = cosTheta;\r\n\r\n  // from tangent-space vector to world-space sample vector\r\n  vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);\r\n  vec3 tangent = normalize(cross(up, N));\r\n  vec3 bitangent = cross(N, tangent);\r\n\r\n  vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z;\r\n  return normalize(sampleVec);\r\n}\r\n\r\n\r\nfloat VanDerCorput(int n, int base)\r\n{\r\n#ifdef WEBGL2\r\n\r\n    uint bits = uint(n);\r\n    bits = (bits << 16u) | (bits >> 16u);\r\n    bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);\r\n    bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);\r\n    bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);\r\n    bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);\r\n    return float(bits) * 2.3283064365386963e-10; // / 0x100000000\r\n\r\n#else\r\n\r\n  float invBase = 1.0 / float(base);\r\n  float denom = 1.0;\r\n  float result = 0.0;\r\n\r\n\r\n  for (int i = 0; i < 32; ++i)\r\n  {\r\n        if (n > 0)\r\n        {\r\n        denom = mod(float(n), 2.0);\r\n        result += denom * invBase;\r\n        invBase = invBase / 2.0;\r\n        n = int(float(n) / 2.0);\r\n        }\r\n  }\r\n\r\n\r\n  return result;\r\n\r\n#endif\r\n}\r\n\r\nvec2 HammersleyNoBitOps(int i, int N)\r\n{\r\n  return vec2(float(i) / float(N), VanDerCorput(i, 2));\r\n}\r\n";

  const { lineDefs } = getStrokeDefs((n, v) => `#define ${n} ${v}\n`);

  const defaultShaders = {
    normalVert,
    normalFrag,
    basicFrag,
    lightVert: lightingShader + lightVert,
    lightTextureFrag,
    phongVert,
    phongFrag: lightingShader + phongFrag,
    fontVert,
    fontFrag,
    lineVert: lineDefs + lineVert,
    lineFrag: lineDefs + lineFrag,
    imageLightVert,
    imageLightDiffusedFrag,
    imageLightSpecularFrag,
    filterBaseVert,
    filterBaseFrag,
  };
  for (const key in defaultShaders) {
    defaultShaders[key] = webgl2CompatibilityShader + defaultShaders[key];
  }

  /**
   * 3D graphics class
   * @private
   * @class p5.RendererGL
   * @extends p5.Renderer
   * @todo extend class to include public method for offscreen
   * rendering (FBO).
   */
  class RendererGL extends Renderer3D {
    constructor(pInst, w, h, isMainCanvas, elt) {
      super(pInst, w, h, isMainCanvas, elt);

      if (this.webglVersion === WEBGL2) {
        this.blendExt = this.GL;
      } else {
        this.blendExt = this.GL.getExtension("EXT_blend_minmax");
      }

      this._userEnabledStencil = false;
      // Store original methods for internal use
      this._internalEnable = this.drawingContext.enable;
      this._internalDisable = this.drawingContext.disable;

      // Override WebGL enable function
      this.drawingContext.enable = (key) => {
        if (key === this.drawingContext.STENCIL_TEST) {
          if (!this._clipping) {
            this._userEnabledStencil = true;
          }
        }
        return this._internalEnable.call(this.drawingContext, key);
      };

      // Override WebGL disable function
      this.drawingContext.disable = (key) => {
        if (key === this.drawingContext.STENCIL_TEST) {
            this._userEnabledStencil = false;
        }
        return this._internalDisable.call(this.drawingContext, key);
      };

      this._cachedBlendMode = undefined;
      this.strandsBackend = glslBackend;
    }

    setupContext() {
      this._setAttributeDefaults(this._pInst);
      this._initContext();
      // This redundant property is useful in reminding you that you are
      // interacting with WebGLRenderingContext, still worth considering future removal
      this.GL = this.drawingContext;
    }

    //////////////////////////////////////////////
    // Rendering
    //////////////////////////////////////////////

    /**
     * @private sets blending in gl context to curBlendMode
     * @param  {Number[]} color [description]
     * @return {Number[]}  Normalized numbers array
     */
    _applyBlendMode () {
      if (this._cachedBlendMode === this.states.curBlendMode) {
        return;
      }
      const gl = this.GL;
      switch (this.states.curBlendMode) {
        case BLEND:
          gl.blendEquation(gl.FUNC_ADD);
          gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
          break;
        case ADD:
          gl.blendEquation(gl.FUNC_ADD);
          gl.blendFunc(gl.ONE, gl.ONE);
          break;
        case REMOVE:
          gl.blendEquation(gl.FUNC_ADD);
          gl.blendFunc(gl.ZERO, gl.ONE_MINUS_SRC_ALPHA);
          break;
        case MULTIPLY:
          gl.blendEquation(gl.FUNC_ADD);
          gl.blendFunc(gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA);
          break;
        case SCREEN:
          gl.blendEquation(gl.FUNC_ADD);
          gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_COLOR);
          break;
        case EXCLUSION:
          gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
          gl.blendFuncSeparate(
            gl.ONE_MINUS_DST_COLOR,
            gl.ONE_MINUS_SRC_COLOR,
            gl.ONE,
            gl.ONE
          );
          break;
        case REPLACE:
          gl.blendEquation(gl.FUNC_ADD);
          gl.blendFunc(gl.ONE, gl.ZERO);
          break;
        case SUBTRACT:
          gl.blendEquationSeparate(gl.FUNC_REVERSE_SUBTRACT, gl.FUNC_ADD);
          gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
          break;
        case DARKEST:
          if (this.blendExt) {
            gl.blendEquationSeparate(
              this.blendExt.MIN || this.blendExt.MIN_EXT,
              gl.FUNC_ADD
            );
            gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
          } else {
            console.warn(
              'blendMode(DARKEST) does not work in your browser in WEBGL mode.'
            );
          }
          break;
        case LIGHTEST:
          if (this.blendExt) {
            gl.blendEquationSeparate(
              this.blendExt.MAX || this.blendExt.MAX_EXT,
              gl.FUNC_ADD
            );
            gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
          } else {
            console.warn(
              'blendMode(LIGHTEST) does not work in your browser in WEBGL mode.'
            );
          }
          break;
        default:
          console.error(
            'Oops! Somehow Renderer3D set curBlendMode to an unsupported mode.'
          );
          break;
      }
      this._cachedBlendMode = this.states.curBlendMode;
    }

    _shaderOptions() {
      return undefined;
    }

    _useShader(shader) {
      const gl = this.GL;
      gl.useProgram(shader._glProgram);
    }

    /**
     * Once all buffers have been bound, this checks to see if there are any
     * remaining active attributes, likely left over from previous renders,
     * and disables them so that they don't affect rendering.
     * @private
     */
    _disableRemainingAttributes(shader) {
      for (const location of this.registerEnabled.values()) {
        if (
          !Object.keys(shader.attributes).some(
            key => shader.attributes[key].location === location
          )
        ) {
          this.GL.disableVertexAttribArray(location);
          this.registerEnabled.delete(location);
        }
      }
    }

    _drawBuffers(geometry, { mode = TRIANGLES, count }) {
      const gl = this.GL;
      const glBuffers = this.geometryBufferCache.getCached(geometry);

      if (!glBuffers) return;

      if (this._curShader.shaderType === 'stroke'){
        if (count === 1) {
          gl.drawArrays(gl.TRIANGLES, 0, geometry.lineVertices.length / 3);
         } else {
         try {
            gl.drawArraysInstanced(
            gl.TRIANGLES,
              0,
              geometry.lineVertices.length / 3,
              count
            );
          } catch (e) {
            console.log(
              "🌸 p5.js says: Instancing is only supported in WebGL2 mode"
            );
          }
         }
      } else if (this._curShader.shaderType === 'text') {
        // Text rendering uses a fixed quad geometry with 6 indices
        this._bindBuffer(glBuffers.indexBuffer, gl.ELEMENT_ARRAY_BUFFER);
        gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
      } else if (glBuffers.indexBuffer) {
        this._bindBuffer(glBuffers.indexBuffer, gl.ELEMENT_ARRAY_BUFFER);

        // If this model is using a Uint32Array we need to ensure the
        // OES_element_index_uint WebGL extension is enabled.
        if (
          this._pInst.webglVersion !== WEBGL2 &&
          glBuffers.indexBufferType === gl.UNSIGNED_INT
        ) {
          if (!gl.getExtension("OES_element_index_uint")) {
            throw new Error(
              "Unable to render a 3d model with > 65535 triangles. Your web browser does not support the WebGL Extension OES_element_index_uint."
            );
          }
        }

        if (count === 1) {
          gl.drawElements(
            gl.TRIANGLES,
            geometry.faces.length * 3,
            glBuffers.indexBufferType,
            0
          );
        } else {
          try {
            gl.drawElementsInstanced(
              gl.TRIANGLES,
              geometry.faces.length * 3,
              glBuffers.indexBufferType,
              0,
              count
            );
          } catch (e) {
            console.log(
              "🌸 p5.js says: Instancing is only supported in WebGL2 mode"
            );
          }
        }
      } else {
        const glMode = mode === TRIANGLES ? gl.TRIANGLES : gl.TRIANGLE_STRIP;
        if (count === 1) {
          gl.drawArrays(glMode, 0, geometry.vertices.length);
        } else {
          try {
            gl.drawArraysInstanced(glMode, 0, geometry.vertices.length, count);
          } catch (e) {
            console.log(
              "🌸 p5.js says: Instancing is only supported in WebGL2 mode"
            );
          }
        }
      }
    }

    //////////////////////////////////////////////
    // Text
    //////////////////////////////////////////////

    _beforeDrawText() {
      this.GL.pixelStorei(this.GL.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
    }
    _afterDrawText() {
      this.GL.pixelStorei(this.GL.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
    }

    //////////////////////////////////////////////
    // Setting
    //////////////////////////////////////////////

    _setAttributeDefaults(pInst) {
      // See issue #3850, safer to enable AA in Safari
      const applyAA = navigator.userAgent.toLowerCase().includes("safari");
      const defaults = {
        alpha: true,
        depth: true,
        stencil: true,
        antialias: applyAA,
        premultipliedAlpha: true,
        preserveDrawingBuffer: true,
        perPixelLighting: true,
        version: 2,
      };
      if (pInst._glAttributes === null) {
        pInst._glAttributes = defaults;
      } else {
        pInst._glAttributes = Object.assign(defaults, pInst._glAttributes);
      }
      return;
    }

    _setAttributes(key, value) {
      if (typeof this._pInst._glAttributes === "undefined") {
        console.log(
          "You are trying to use setAttributes on a p5.Graphics object " +
            "that does not use a WEBGL renderer."
        );
        return;
      }
      let unchanged = true;
      if (typeof value !== "undefined") {
        //first time modifying the attributes
        if (this._pInst._glAttributes === null) {
          this._pInst._glAttributes = {};
        }
        if (this._pInst._glAttributes[key] !== value) {
          //changing value of previously altered attribute
          this._pInst._glAttributes[key] = value;
          unchanged = false;
        }
        //setting all attributes with some change
      } else if (key instanceof Object) {
        if (this._pInst._glAttributes !== key) {
          this._pInst._glAttributes = key;
          unchanged = false;
        }
      }
      //@todo_FES
      if (!this.isP3D || unchanged) {
        return;
      }

      if (!this._pInst._setupDone) {
        if (this.geometryBufferCache.numCached() > 0) {
          p5._friendlyError(
            "Sorry, Could not set the attributes, you need to call setAttributes() " +
              "before calling the other drawing methods in setup()"
          );
          return;
        }
      }

      this._resetContext(null, null, RendererGL);

      if (this.states.curCamera) {
        this.states.curCamera._renderer = this._renderer;
      }
    }

    _initContext() {
      if (this._pInst._glAttributes?.version !== 1) {
        // Unless WebGL1 is explicitly asked for, try to create a WebGL2 context
        this.drawingContext = this.canvas.getContext(
          "webgl2",
          this._pInst._glAttributes
        );
      }
      this.webglVersion = this.drawingContext
        ? WEBGL2
        : WEBGL;
      // If this is the main canvas, make sure the global `webglVersion` is set
      this._pInst.webglVersion = this.webglVersion;
      if (!this.drawingContext) {
        // If we were unable to create a WebGL2 context (either because it was
        // disabled via `setAttributes({ version: 1 })` or because the device
        // doesn't support it), fall back to a WebGL1 context
        this.drawingContext =
          this.canvas.getContext("webgl", this._pInst._glAttributes) ||
          this.canvas.getContext("experimental-webgl", this._pInst._glAttributes);
      }
      if (this.drawingContext === null) {
        throw new Error("Error creating webgl context");
      } else {
        const gl = this.drawingContext;
        gl.enable(gl.DEPTH_TEST);
        gl.depthFunc(gl.LEQUAL);
        gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
        // Make sure all images are loaded into the canvas premultiplied so that
        // they match the way we render colors. This will make framebuffer textures
        // be encoded the same way as textures from everything else.
        gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
        this._viewport = this.drawingContext.getParameter(
          this.drawingContext.VIEWPORT
        );
      }
    }

    _updateSize() {}

    _getMaxTextureSize() {
      const gl = this.drawingContext;
      return gl.getParameter(gl.MAX_TEXTURE_SIZE);
    }

    _adjustDimensions(width, height) {
      if (!this._maxTextureSize) {
        this._maxTextureSize = this._getMaxTextureSize();
      }
      let maxTextureSize = this._maxTextureSize;

      let maxAllowedPixelDimensions = Math.floor(
        maxTextureSize / this._pixelDensity
      );
      let adjustedWidth = Math.min(width, maxAllowedPixelDimensions);
      let adjustedHeight = Math.min(height, maxAllowedPixelDimensions);

      if (adjustedWidth !== width || adjustedHeight !== height) {
        console.warn(
          "Warning: The requested width/height exceeds hardware limits. " +
            `Adjusting dimensions to width: ${adjustedWidth}, height: ${adjustedHeight}.`
        );
      }

      return { adjustedWidth, adjustedHeight };
    }

    _resetBuffersBeforeDraw() {
      this.GL.clearStencil(0);
      this.GL.clear(this.GL.DEPTH_BUFFER_BIT | this.GL.STENCIL_BUFFER_BIT);
      if (!this._userEnabledStencil) {
        this._internalDisable.call(this.GL, this.GL.STENCIL_TEST);
      }
    }

    _applyClip() {
      const gl = this.GL;
      gl.clearStencil(0);
      gl.clear(gl.STENCIL_BUFFER_BIT);
      this._internalEnable.call(gl, gl.STENCIL_TEST);
      this._stencilTestOn = true;
      gl.stencilFunc(
        gl.ALWAYS, // the test
        1, // reference value
        0xff // mask
      );
      gl.stencilOp(
        gl.KEEP, // what to do if the stencil test fails
        gl.KEEP, // what to do if the depth test fails
        gl.REPLACE // what to do if both tests pass
      );
      gl.disable(gl.DEPTH_TEST);
    }

    _unapplyClip() {
      const gl = this.GL;
      gl.stencilOp(
        gl.KEEP, // what to do if the stencil test fails
        gl.KEEP, // what to do if the depth test fails
        gl.KEEP // what to do if both tests pass
      );
      gl.stencilFunc(
        this._clipInvert ? gl.EQUAL : gl.NOTEQUAL, // the test
        0, // reference value
        0xff // mask
      );
      gl.enable(gl.DEPTH_TEST);
    }

    _clearClipBuffer() {
      this.GL.clearStencil(1);
      this.GL.clear(this.GL.STENCIL_BUFFER_BIT);
      if (!this._userEnabledStencil) {
        this._internalDisable.call(this.GL, this.GL.STENCIL_TEST);
      }
    }

    // x,y are canvas-relative (pre-scaled by _pixelDensity)
    _getPixel(x, y) {
      const gl = this.GL;
      return readPixelWebGL(
        gl,
        null,
        x,
        y,
        gl.RGBA,
        gl.UNSIGNED_BYTE,
        this._pInst.height * this._pInst.pixelDensity()
      );
    }

    /**
     * Loads the pixels data for this canvas into the pixels[] attribute.
     * Note that updatePixels() and set() do not work.
     * Any pixel manipulation must be done directly to the pixels[] array.
     *
     * @private
     */
    loadPixels() {
      //@todo_FES
      if (this._pInst._glAttributes.preserveDrawingBuffer !== true) {
        console.log(
          "loadPixels only works in WebGL when preserveDrawingBuffer " +
            "is true."
        );
        return;
      }

      const pd = this._pixelDensity;
      const gl = this.GL;

      this.pixels = readPixelsWebGL(
        this.pixels,
        gl,
        null,
        0,
        0,
        this.width * pd,
        this.height * pd,
        gl.RGBA,
        gl.UNSIGNED_BYTE,
        this.height * pd
      );
    }

    updatePixels() {
      const fbo = this._getTempFramebuffer();
      fbo.pixels = this.pixels;
      fbo.updatePixels();
      this.push();
      this.resetMatrix();
      this.clear();
      this.states.setValue("imageMode", CORNER);
      this.image(
        fbo,
        0,
        0,
        fbo.width,
        fbo.height,
        -fbo.width / 2,
        -fbo.height / 2,
        fbo.width,
        fbo.height
      );
      this.pop();
      this.GL.clearDepth(1);
      this.GL.clear(this.GL.DEPTH_BUFFER_BIT);
    }

    zClipRange() {
      return [-1, 1];
    }
    defaultNearScale() {
      return 0.1;
    }
    defaultFarScale() {
      return 10;
    }

    viewport(w, h) {
      this._viewport = [0, 0, w, h];
      this.GL.viewport(0, 0, w, h);
    }

    _updateViewport() {
      this._origViewport = {
        width: this.GL.drawingBufferWidth,
        height: this.GL.drawingBufferHeight,
      };
      this.viewport(this._origViewport.width, this._origViewport.height);
    }

    _createPixelsArray() {
      this.pixels = new Uint8Array(
        this.GL.drawingBufferWidth * this.GL.drawingBufferHeight * 4
      );
    }

    /**
     * clears color and depth buffers
     * with r,g,b,a
     * @private
     * @param {Number} r normalized red val.
     * @param {Number} g normalized green val.
     * @param {Number} b normalized blue val.
     * @param {Number} a normalized alpha val.
     */
    clear(...args) {
      const _r = args[0] || 0;
      const _g = args[1] || 0;
      const _b = args[2] || 0;
      let _a = args[3] || 0;

      const activeFramebuffer = this.activeFramebuffer();
      if (
        activeFramebuffer &&
        activeFramebuffer.format === UNSIGNED_BYTE &&
        !activeFramebuffer.antialias &&
        _a === 0
      ) {
        // Drivers on Intel Macs check for 0,0,0,0 exactly when drawing to a
        // framebuffer and ignore the command if it's the only drawing command to
        // the framebuffer. To work around it, we can set the alpha to a value so
        // low that it still rounds down to 0, but that circumvents the buggy
        // check in the driver.
        _a = 1e-10;
      }

      this.GL.clearColor(_r * _a, _g * _a, _b * _a, _a);
      this.GL.clearDepth(1);
      this.GL.clear(this.GL.COLOR_BUFFER_BIT | this.GL.DEPTH_BUFFER_BIT);
    }

    /**
     * Resets all depth information so that nothing previously drawn will
     * occlude anything subsequently drawn.
     */
    clearDepth(depth = 1) {
      this.GL.clearDepth(depth);
      this.GL.clear(this.GL.DEPTH_BUFFER_BIT);
    }

    _applyStencilTestIfClipping() {
      const drawTarget = this.drawTarget();
      if (drawTarget._isClipApplied !== this._stencilTestOn) {
        if (drawTarget._isClipApplied) {
          this._internalEnable.call(this.GL, this.GL.STENCIL_TEST);
          this._stencilTestOn = true;
        } else {
          if (!this._userEnabledStencil) {
            this._internalDisable.call(this.GL, this.GL.STENCIL_TEST);
          }
          this._stencilTestOn = false;
        }
      }
    }


    //////////////////////////////////////////////
    // SHADER
    //////////////////////////////////////////////

    /*
     * shaders are created and cached on a per-renderer basis,
     * on the grounds that each renderer will have its own gl context
     * and the shader must be valid in that context.
     */

    baseMaterialShader() {
      if (!this._pInst._glAttributes.perPixelLighting) {
        throw new Error(
          "The material shader does not support hooks without perPixelLighting. Try turning it back on."
        );
      }
      return super.baseMaterialShader();
    }

    _getLightShader() {
      if (!this._defaultLightShader) {
        if (this._pInst._glAttributes.perPixelLighting) {
          this._defaultLightShader = new Shader(
            this,
            this._webGL2CompatibilityPrefix("vert", "highp") +
              defaultShaders.phongVert,
            this._webGL2CompatibilityPrefix("frag", "highp") +
              defaultShaders.phongFrag,
            {
              vertex: {
                "void beforeVertex": "() {}",
                "Vertex getObjectInputs": "(Vertex inputs) { return inputs; }",
                "Vertex getWorldInputs": "(Vertex inputs) { return inputs; }",
                "Vertex getCameraInputs": "(Vertex inputs) { return inputs; }",
                "void afterVertex": "() {}",
              },
              fragment: {
                "void beforeFragment": "() {}",
                "Inputs getPixelInputs": "(Inputs inputs) { return inputs; }",
                "vec4 combineColors": `(ColorComponents components) {
                vec4 color = vec4(0.);
                color.rgb += components.diffuse * components.baseColor;
                color.rgb += components.ambient * components.ambientColor;
                color.rgb += components.specular * components.specularColor;
                color.rgb += components.emissive;
                color.a = components.opacity;
                return color;
              }`,
                "vec4 getFinalColor": "(vec4 color) { return color; }",
                "void afterFragment": "() {}",
              },
            }
          );
        } else {
          this._defaultLightShader = new Shader(
            this,
            this._webGL2CompatibilityPrefix("vert", "highp") +
              defaultShaders.lightVert,
            this._webGL2CompatibilityPrefix("frag", "highp") +
              defaultShaders.lightTextureFrag
          );
        }
      }

      return this._defaultLightShader;
    }

    _getNormalShader() {
      if (!this._defaultNormalShader) {
        this._defaultNormalShader = new Shader(
          this,
          this._webGL2CompatibilityPrefix("vert", "highp") +
            defaultShaders.normalVert,
          this._webGL2CompatibilityPrefix("frag", "highp") +
            defaultShaders.normalFrag,
          {
            vertex: {
              "void beforeVertex": "() {}",
              "Vertex getObjectInputs": "(Vertex inputs) { return inputs; }",
              "Vertex getWorldInputs": "(Vertex inputs) { return inputs; }",
              "Vertex getCameraInputs": "(Vertex inputs) { return inputs; }",
              "void afterVertex": "() {}",
            },
            fragment: {
              "void beforeFragment": "() {}",
              "vec4 getFinalColor": "(vec4 color) { return color; }",
              "void afterFragment": "() {}",
            },
          }
        );
      }

      return this._defaultNormalShader;
    }

    _getColorShader() {
      if (!this._defaultColorShader) {
        this._defaultColorShader = new Shader(
          this,
          this._webGL2CompatibilityPrefix("vert", "highp") +
            defaultShaders.normalVert,
          this._webGL2CompatibilityPrefix("frag", "highp") +
            defaultShaders.basicFrag,
          {
            vertex: {
              "void beforeVertex": "() {}",
              "Vertex getObjectInputs": "(Vertex inputs) { return inputs; }",
              "Vertex getWorldInputs": "(Vertex inputs) { return inputs; }",
              "Vertex getCameraInputs": "(Vertex inputs) { return inputs; }",
              "void afterVertex": "() {}",
            },
            fragment: {
              "void beforeFragment": "() {}",
              "vec4 getFinalColor": "(vec4 color) { return color; }",
              "void afterFragment": "() {}",
            },
          }
        );
      }

      return this._defaultColorShader;
    }

    _getLineShader() {
      if (!this._defaultLineShader) {
        this._defaultLineShader = new Shader(
          this,
          this._webGL2CompatibilityPrefix("vert", "highp") +
            defaultShaders.lineVert,
          this._webGL2CompatibilityPrefix("frag", "highp") +
            defaultShaders.lineFrag,
          {
            vertex: {
              "void beforeVertex": "() {}",
              "StrokeVertex getObjectInputs":
                "(StrokeVertex inputs) { return inputs; }",
              "StrokeVertex getWorldInputs":
                "(StrokeVertex inputs) { return inputs; }",
              "StrokeVertex getCameraInputs":
                "(StrokeVertex inputs) { return inputs; }",
              "void afterVertex": "() {}",
            },
            fragment: {
              "void beforeFragment": "() {}",
              "Inputs getPixelInputs": "(Inputs inputs) { return inputs; }",
              "vec4 getFinalColor": "(vec4 color) { return color; }",
              "bool shouldDiscard": "(bool outside) { return outside; }",
              "void afterFragment": "() {}",
            },
          }
        );
      }

      return this._defaultLineShader;
    }

    _getFontShader() {
      if (!this._defaultFontShader) {
        if (this.webglVersion === WEBGL) {
          this.GL.getExtension("OES_standard_derivatives");
        }
        this._defaultFontShader = new Shader(
          this,
          this._webGL2CompatibilityPrefix("vert", "highp") +
            defaultShaders.fontVert,
          this._webGL2CompatibilityPrefix("frag", "highp") +
            defaultShaders.fontFrag
        );
      }
      return this._defaultFontShader;
    }

    baseFilterShader() {
      if (!this._baseFilterShader) {
        this._baseFilterShader = new Shader(
          this,
          this._webGL2CompatibilityPrefix("vert", "highp") +
            defaultShaders.filterBaseVert,
          this._webGL2CompatibilityPrefix("frag", "highp") +
            defaultShaders.filterBaseFrag,
          {
            vertex: {},
            fragment: {
              "vec4 getColor": `(FilterInputs inputs, in sampler2D canvasContent) {
              return getTexture(canvasContent, inputs.texCoord);
            }`,
            },
            hookAliases: {
              'getColor': ['filterColor'],
            },
          }
        );
      }
      return this._baseFilterShader;
    }

    _webGL2CompatibilityPrefix(shaderType, floatPrecision) {
      let code = "";
      if (this.webglVersion === WEBGL2) {
        code += "#version 300 es\n#define WEBGL2\n";
      }
      if (shaderType === "vert") {
        code += "#define VERTEX_SHADER\n";
      } else if (shaderType === "frag") {
        code += "#define FRAGMENT_SHADER\n";
      }
      if (floatPrecision) {
        code += `precision ${floatPrecision} float;\n`;
      }
      return code;
    }

    /*
     * WebGL-specific implementation of imageLight shader creation
     */
    _createImageLightShader(type) {
      if (type === 'diffused') {
        return new Shader(
          this,
          this._webGL2CompatibilityPrefix("vert", "highp") +
            defaultShaders.imageLightVert,
          this._webGL2CompatibilityPrefix("frag", "highp") +
            defaultShaders.imageLightDiffusedFrag
        );
      } else if (type === 'specular') {
        return new Shader(
          this,
          this._webGL2CompatibilityPrefix("vert", "highp") +
            defaultShaders.imageLightVert,
          this._webGL2CompatibilityPrefix("frag", "highp") +
            defaultShaders.imageLightSpecularFrag
        );
      }
      throw new Error(`Unknown imageLight shader type: ${type}`);
    }


    /*
     * WebGL-specific implementation of mipmap texture creation
     */
    _createMipmapTexture(levels) {
      return new MipmapTexture(this, levels, {});
    }

    /*
     * Prepare array to collect ImageData levels for WebGL
     */
    _prepareMipmapData(size, mipLevels) {
      return { levels: [], size, mipLevels };
    }

    /*
     * Accumulate ImageData from framebuffer for WebGL
     */
    _accumulateMipLevel(framebuffer, mipmapData, mipLevel, width, height) {
      const imageData = framebuffer.get().drawingContext.getImageData(0, 0, width, height);
      mipmapData.levels.push(imageData);
    }

    /*
     * Create final MipmapTexture from collected ImageData for WebGL
     */
    _finalizeMipmapTexture(mipmapData) {
      return new MipmapTexture(this, mipmapData.levels, {
        minFilter: LINEAR_MIPMAP,
        magFilter: LINEAR,
      });
    }

    createMipmapTextureHandle({ levels, format, dataType, width, height }) {
      const gl = this.GL;
      const texture = gl.createTexture();

      gl.bindTexture(gl.TEXTURE_2D, texture);

      // Determine GL format and data type
      const glFormat = gl.RGBA;
      const glDataType = gl.UNSIGNED_BYTE;

      for (let level = 0; level < levels.length; level++) {
        gl.texImage2D(
          gl.TEXTURE_2D,
          level,
          glFormat,
          glFormat,
          glDataType,
          levels[level]
        );
      }

      // Set mipmap-appropriate filtering
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);

      gl.bindTexture(gl.TEXTURE_2D, null);

      return { texture, glFormat, glDataType };
    }

    /* Binds a buffer to the drawing context
     * when passed more than two arguments it also updates or initializes
     * the data associated with the buffer
     */
    _bindBuffer(buffer, target, values, type, usage) {
      const gl = this.GL;
      if (!target) target = gl.ARRAY_BUFFER;
      gl.bindBuffer(target, buffer);

      if (values !== undefined) {
        const data = this._normalizeBufferData(values, type);
        gl.bufferData(target, data, usage || gl.STATIC_DRAW);
      }
    }

    _prepareBuffer(renderBuffer, geometry, shader) {
      const attributes = shader.attributes;
      const gl = this.GL;
      const glBuffers = this._getOrMakeCachedBuffers(geometry);

      // loop through each of the buffer definitions
      const attr = attributes[renderBuffer.attr];
      if (!attr) {
        return;
      }
      // check if the geometry has the appropriate source array
      let buffer = glBuffers[renderBuffer.dst];
      const src = geometry[renderBuffer.src];
      if (src && src.length > 0) {
        // check if we need to create the GL buffer
        const createBuffer = !buffer;
        if (createBuffer) {
          // create and remember the buffer
          glBuffers[renderBuffer.dst] = buffer = gl.createBuffer();
        }
        // bind the buffer
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

        // check if we need to fill the buffer with data
        if (createBuffer || geometry.dirtyFlags[renderBuffer.src] !== false) {
          const map = renderBuffer.map;
          // get the values from the geometry, possibly transformed
          const values = map ? map(src) : src;
          // fill the buffer with the values
          this._bindBuffer(buffer, gl.ARRAY_BUFFER, values);
          // mark the geometry's source array as clean
          geometry.dirtyFlags[renderBuffer.src] = false;
        }
        // enable the attribute
        shader.enableAttrib(attr, renderBuffer.size);
      } else {
        const loc = attr.location;
        if (loc === -1 || !this.registerEnabled.has(loc)) {
          return;
        }
        // Disable register corresponding to unused attribute
        gl.disableVertexAttribArray(loc);
        // Record register availability
        this.registerEnabled.delete(loc);
      }
    }

    _enableAttrib(_shader, attr, size, type, normalized, stride, offset) {
      const loc = attr.location;
      const gl = this.GL;
      // Enable register even if it is disabled
      if (!this.registerEnabled.has(loc)) {
        gl.enableVertexAttribArray(loc);
        // Record register availability
        this.registerEnabled.add(loc);
      }
      gl.vertexAttribPointer(
        loc,
        size,
        type || gl.FLOAT,
        normalized || false,
        stride || 0,
        offset || 0
      );
    }

    _ensureGeometryBuffers(buffers, indices, indexType) {
      const gl = this.GL;

      if (indices) {
        let buffer = buffers.indexBuffer;
        if (!buffer) buffer = gl.createBuffer();
        this._bindBuffer(buffer, gl.ELEMENT_ARRAY_BUFFER, indices, indexType);

        buffers.indexBuffer = buffer;

        // If we're using a Uint32Array for our indexBuffer we will need to pass a
        // different enum value to WebGL draw triangles. This happens in
        // the _drawElements function.
        buffers.indexBufferType = indexType === Uint32Array ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT;
      } else if (buffers.indexBuffer) {
        // the index buffer is unused, remove it
        gl.deleteBuffer(buffers.indexBuffer);
        buffers.indexBuffer = null;
      }
    }

    _freeBuffers(buffers) {
      const gl = this.GL;
      if (buffers.indexBuffer) {
        gl.deleteBuffer(buffers.indexBuffer);
      }

      function freeBuffers(defs) {
        for (const def of defs) {
          if (buffers[def.dst]) {
            gl.deleteBuffer(buffers[def.dst]);
            buffers[def.dst] = null;
          }
        }
      }

      // free all the buffers
      freeBuffers(this.buffers.stroke);
      freeBuffers(this.buffers.fill);
      freeBuffers(this.buffers.user);
    }

    _initShader(shader) {
      const gl = this.GL;

      const vertShader = gl.createShader(gl.VERTEX_SHADER);
      gl.shaderSource(vertShader, shader.vertSrc());
      gl.compileShader(vertShader);
      if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) {
        throw new Error(`Yikes! An error occurred compiling the vertex shader: ${
        gl.getShaderInfoLog(vertShader)
      } in:\n\n${shader.vertSrc()}`);
      }

      const fragShader = gl.createShader(gl.FRAGMENT_SHADER);
      gl.shaderSource(fragShader, shader.fragSrc());
      gl.compileShader(fragShader);
      if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS)) {
        throw new Error(`Darn! An error occurred compiling the fragment shader: ${
        gl.getShaderInfoLog(fragShader)
      }`);
      }

      const program = gl.createProgram();
      gl.attachShader(program, vertShader);
      gl.attachShader(program, fragShader);
      gl.linkProgram(program);

      if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        throw new Error(
          `Snap! Error linking shader program: ${gl.getProgramInfoLog(program)}`
        );
      }

      shader._glProgram = program;
      shader._vertShader = vertShader;
      shader._fragShader = fragShader;
    }

    _finalizeShader() {}

    _getShaderAttributes(shader) {
      return getWebGLShaderAttributes(shader, this.GL);
    }

    getUniformMetadata(shader) {
      return getWebGLUniformMetadata(shader, this.GL);
    }

    updateUniformValue(shader, uniform, data) {
      return setWebGLUniformValue(
        shader,
        uniform,
        data,
        (tex) => this.getTexture(tex),
        this.GL
      );
    }

    _updateTexture(uniform, tex) {
      const gl = this.GL;
      gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex);
      tex.bindTexture();
      tex.update();
      gl.uniform1i(uniform.location, uniform.samplerIndex);
    }

    bindTexture(tex) {
      // bind texture using gl context + glTarget and
      // generated gl texture object
      this.GL.bindTexture(this.GL.TEXTURE_2D, tex.getTexture().texture);
    }

    unbindTexture() {
      // unbind per above, disable texturing on glTarget
      this.GL.bindTexture(this.GL.TEXTURE_2D, null);
    }

    _unbindFramebufferTexture(uniform) {
      // Make sure an empty texture is bound to the slot so that we don't
      // accidentally leave a framebuffer bound, causing a feedback loop
      // when something else tries to write to it
      const gl = this.GL;
      const empty = this._getEmptyTexture();
      gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex);
      empty.bindTexture();
      gl.uniform1i(uniform.location, uniform.samplerIndex);
    }

    createTexture({ width, height, format, dataType }) {
      const gl = this.GL;
      const tex = gl.createTexture();
      gl.bindTexture(gl.TEXTURE_2D, tex);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0,
                         gl.RGBA, gl.UNSIGNED_BYTE, null);
      // TODO use format and data type
      return { texture: tex, glFormat: gl.RGBA, glDataType: gl.UNSIGNED_BYTE };
    }

    createFramebufferTextureHandle(framebufferTexture) {
      // For WebGL, framebuffer texture handles are designed to be null
      return null;
    }

    uploadTextureFromSource({ texture, glFormat, glDataType }, source) {
      const gl = this.GL;
      gl.texImage2D(gl.TEXTURE_2D, 0, glFormat, glFormat, glDataType, source);
    }

    uploadTextureFromData({ texture, glFormat, glDataType }, data, width, height) {
      const gl = this.GL;
      gl.texImage2D(
        gl.TEXTURE_2D,
        0,
        glFormat,
        width,
        height,
        0,
        glFormat,
        glDataType,
        data
      );
    }

    getSampler(_texture) {
      return undefined;
    }

    bindTextureToShader({ texture }, sampler, uniformName, unit) {
      const gl = this.GL;
      gl.activeTexture(gl.TEXTURE0 + unit);
      gl.bindTexture(gl.TEXTURE_2D, texture);
      const location = gl.getUniformLocation(glProgram, uniformName);
      gl.uniform1i(location, unit);
    }

    setTextureParams(texture) {
      return setWebGLTextureParams(texture, this.GL, this.webglVersion);
    }

    deleteTexture({ texture }) {
      this.GL.deleteTexture(texture);
    }


    /**
     * @private blends colors according to color components.
     * If alpha value is less than 1, or non-standard blendMode
     * we need to enable blending on our gl context.
     * @param  {Number[]} color The currently set color, with values in 0-1 range
     * @param  {Boolean} [hasTransparency] Whether the shape being drawn has other
     * transparency internally, e.g. via vertex colors
     * @return {Number[]}  Normalized numbers array
     */
    _applyColorBlend(colors, hasTransparency) {
      const gl = this.GL;

      const isTexture = this.states.drawMode === TEXTURE;
      const doBlend =
        hasTransparency ||
        this.states.userFillShader ||
        this.states.userStrokeShader ||
        isTexture ||
        this.states.curBlendMode !== BLEND ||
        colors[colors.length - 1] < 1.0 ||
        this._isErasing;

      if (doBlend !== this._isBlending) {
        if (
          doBlend ||
          (this.states.curBlendMode !== BLEND &&
            this.states.curBlendMode !== ADD)
        ) {
          gl.enable(gl.BLEND);
        } else {
          gl.disable(gl.BLEND);
        }
        gl.depthMask(true);
        this._isBlending = doBlend;
      }
      this._applyBlendMode();
      return colors;
    }

    //////////////////////////////////////////////
    // Shader hooks
    //////////////////////////////////////////////
    uniformNameFromHookKey(key) {
      return key.slice(key.indexOf(' ') + 1);
    }
    populateHooks(shader, src, shaderType) {
      return populateGLSLHooks(shader, src, shaderType);
    }

    getShaderHookTypes(shader, hookName) {
      return getShaderHookTypes(shader, hookName);
    }

    //////////////////////////////////////////////
    // Framebuffer methods
    //////////////////////////////////////////////

    defaultFramebufferAlpha() {
      return this._pInst._glAttributes.alpha;
    }

    defaultFramebufferAntialias() {
      return this.supportsFramebufferAntialias()
        ? this._pInst._glAttributes.antialias
        : false;
    }

    supportsFramebufferAntialias() {
      return this.webglVersion === WEBGL2;
    }

    createFramebufferResources(framebuffer) {
      const gl = this.GL;

      framebuffer.framebuffer = gl.createFramebuffer();
      if (!framebuffer.framebuffer) {
        throw new Error('Unable to create a framebuffer');
      }

      if (framebuffer.antialias) {
        framebuffer.aaFramebuffer = gl.createFramebuffer();
        if (!framebuffer.aaFramebuffer) {
          throw new Error('Unable to create a framebuffer for antialiasing');
        }
      }
    }

    validateFramebufferFormats(framebuffer) {
      const gl = this.GL;

      if (
        framebuffer.useDepth &&
        this.webglVersion === WEBGL &&
        !gl.getExtension('WEBGL_depth_texture')
      ) {
        console.warn(
          'Unable to create depth textures in this environment. Falling back ' +
            'to a framebuffer without depth.'
        );
        framebuffer.useDepth = false;
      }

      if (
        framebuffer.useDepth &&
        this.webglVersion === WEBGL &&
        framebuffer.depthFormat === FLOAT
      ) {
        console.warn(
          'FLOAT depth format is unavailable in WebGL 1. ' +
            'Defaulting to UNSIGNED_INT.'
        );
        framebuffer.depthFormat = UNSIGNED_INT;
      }

      if (![
        UNSIGNED_BYTE,
        FLOAT,
        HALF_FLOAT
      ].includes(framebuffer.format)) {
        console.warn(
          'Unknown Framebuffer format. ' +
            'Please use UNSIGNED_BYTE, FLOAT, or HALF_FLOAT. ' +
            'Defaulting to UNSIGNED_BYTE.'
        );
        framebuffer.format = UNSIGNED_BYTE;
      }
      if (framebuffer.useDepth && ![
        UNSIGNED_INT,
        FLOAT
      ].includes(framebuffer.depthFormat)) {
        console.warn(
          'Unknown Framebuffer depth format. ' +
            'Please use UNSIGNED_INT or FLOAT. Defaulting to FLOAT.'
        );
        framebuffer.depthFormat = FLOAT;
      }

      const support = checkWebGLCapabilities(this);
      if (!support.float && framebuffer.format === FLOAT) {
        console.warn(
          'This environment does not support FLOAT textures. ' +
            'Falling back to UNSIGNED_BYTE.'
        );
        framebuffer.format = UNSIGNED_BYTE;
      }
      if (
        framebuffer.useDepth &&
        !support.float &&
        framebuffer.depthFormat === FLOAT
      ) {
        console.warn(
          'This environment does not support FLOAT depth textures. ' +
            'Falling back to UNSIGNED_INT.'
        );
        framebuffer.depthFormat = UNSIGNED_INT;
      }
      if (!support.halfFloat && framebuffer.format === HALF_FLOAT) {
        console.warn(
          'This environment does not support HALF_FLOAT textures. ' +
            'Falling back to UNSIGNED_BYTE.'
        );
        framebuffer.format = UNSIGNED_BYTE;
      }

      if (
        framebuffer.channels === RGB &&
        [FLOAT, HALF_FLOAT].includes(framebuffer.format)
      ) {
        console.warn(
          'FLOAT and HALF_FLOAT formats do not work cross-platform with only ' +
            'RGB channels. Falling back to RGBA.'
        );
        framebuffer.channels = RGBA;
      }
    }

    recreateFramebufferTextures(framebuffer) {
      const gl = this.GL;

      const prevBoundTexture = gl.getParameter(gl.TEXTURE_BINDING_2D);
      const prevBoundFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);

      const colorTexture = gl.createTexture();
      if (!colorTexture) {
        throw new Error('Unable to create color texture');
      }
      gl.bindTexture(gl.TEXTURE_2D, colorTexture);
      const colorFormat = this._getFramebufferColorFormat(framebuffer);
      gl.texImage2D(
        gl.TEXTURE_2D,
        0,
        colorFormat.internalFormat,
        framebuffer.width * framebuffer.density,
        framebuffer.height * framebuffer.density,
        0,
        colorFormat.format,
        colorFormat.type,
        null
      );
      framebuffer.colorTexture = colorTexture;
      gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer.framebuffer);
      gl.framebufferTexture2D(
        gl.FRAMEBUFFER,
        gl.COLOR_ATTACHMENT0,
        gl.TEXTURE_2D,
        colorTexture,
        0
      );

      if (framebuffer.useDepth) {
        // Create the depth texture
        const depthTexture = gl.createTexture();
        if (!depthTexture) {
          throw new Error('Unable to create depth texture');
        }
        const depthFormat = this._getFramebufferDepthFormat(framebuffer);
        gl.bindTexture(gl.TEXTURE_2D, depthTexture);
        gl.texImage2D(
          gl.TEXTURE_2D,
          0,
          depthFormat.internalFormat,
          framebuffer.width * framebuffer.density,
          framebuffer.height * framebuffer.density,
          0,
          depthFormat.format,
          depthFormat.type,
          null
        );

        gl.framebufferTexture2D(
          gl.FRAMEBUFFER,
          framebuffer.useStencil ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT,
          gl.TEXTURE_2D,
          depthTexture,
          0
        );
        framebuffer.depthTexture = depthTexture;
      }

      // Create separate framebuffer for antialiasing
      if (framebuffer.antialias) {
        framebuffer.colorRenderbuffer = gl.createRenderbuffer();
        gl.bindRenderbuffer(gl.RENDERBUFFER, framebuffer.colorRenderbuffer);
        gl.renderbufferStorageMultisample(
          gl.RENDERBUFFER,
          Math.max(
            0,
            Math.min(framebuffer.antialiasSamples, gl.getParameter(gl.MAX_SAMPLES))
          ),
          colorFormat.internalFormat,
          framebuffer.width * framebuffer.density,
          framebuffer.height * framebuffer.density
        );

        if (framebuffer.useDepth) {
          const depthFormat = this._getFramebufferDepthFormat(framebuffer);
          framebuffer.depthRenderbuffer = gl.createRenderbuffer();
          gl.bindRenderbuffer(gl.RENDERBUFFER, framebuffer.depthRenderbuffer);
          gl.renderbufferStorageMultisample(
            gl.RENDERBUFFER,
            Math.max(
              0,
              Math.min(framebuffer.antialiasSamples, gl.getParameter(gl.MAX_SAMPLES))
            ),
            depthFormat.internalFormat,
            framebuffer.width * framebuffer.density,
            framebuffer.height * framebuffer.density
          );
        }

        gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer.aaFramebuffer);
        gl.framebufferRenderbuffer(
          gl.FRAMEBUFFER,
          gl.COLOR_ATTACHMENT0,
          gl.RENDERBUFFER,
          framebuffer.colorRenderbuffer
        );
        if (framebuffer.useDepth) {
          gl.framebufferRenderbuffer(
            gl.FRAMEBUFFER,
            framebuffer.useStencil ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT,
            gl.RENDERBUFFER,
            framebuffer.depthRenderbuffer
          );
        }
      }

      gl.bindTexture(gl.TEXTURE_2D, prevBoundTexture);
      gl.bindFramebuffer(gl.FRAMEBUFFER, prevBoundFramebuffer);
    }

    /**
     * To create a WebGL texture, one needs to supply three pieces of information:
     * the type (the data type each channel will be stored as, e.g. int or float),
     * the format (the color channels that will each be stored in the previously
     * specified type, e.g. rgb or rgba), and the internal format (the specifics
     * of how data for each channel, in the aforementioned type, will be packed
     * together, such as how many bits to use, e.g. RGBA32F or RGB565.)
     *
     * The format and channels asked for by the user hint at what these values
     * need to be, and the WebGL version affects what options are avaiable.
     * This method returns the values for these three properties, given the
     * framebuffer's settings.
     *
     * @private
     */
    _getFramebufferColorFormat(framebuffer) {
      let type, format, internalFormat;
      const gl = this.GL;

      if (framebuffer.format === FLOAT) {
        type = gl.FLOAT;
      } else if (framebuffer.format === HALF_FLOAT) {
        type = this.webglVersion === WEBGL2
          ? gl.HALF_FLOAT
          : gl.getExtension('OES_texture_half_float').HALF_FLOAT_OES;
      } else {
        type = gl.UNSIGNED_BYTE;
      }

      if (framebuffer.channels === RGBA) {
        format = gl.RGBA;
      } else {
        format = gl.RGB;
      }

      if (this.webglVersion === WEBGL2) {
        // https://webgl2fundamentals.org/webgl/lessons/webgl-data-textures.html
        const table = {
          [gl.FLOAT]: {
            [gl.RGBA]: gl.RGBA32F
            // gl.RGB32F is not available in Firefox without an alpha channel
          },
          [gl.HALF_FLOAT]: {
            [gl.RGBA]: gl.RGBA16F
            // gl.RGB16F is not available in Firefox without an alpha channel
          },
          [gl.UNSIGNED_BYTE]: {
            [gl.RGBA]: gl.RGBA8, // gl.RGBA4
            [gl.RGB]: gl.RGB8 // gl.RGB565
          }
        };
        internalFormat = table[type][format];
      } else if (framebuffer.format === HALF_FLOAT) {
        internalFormat = gl.RGBA;
      } else {
        internalFormat = format;
      }

      return { internalFormat, format, type };
    }

    /**
     * To create a WebGL texture, one needs to supply three pieces of information:
     * the type (the data type each channel will be stored as, e.g. int or float),
     * the format (the color channels that will each be stored in the previously
     * specified type, e.g. rgb or rgba), and the internal format (the specifics
     * of how data for each channel, in the aforementioned type, will be packed
     * together, such as how many bits to use, e.g. RGBA32F or RGB565.)
     *
     * This method takes into account the settings asked for by the user and
     * returns values for these three properties that can be used for the
     * texture storing depth information.
     *
     * @private
     */
    _getFramebufferDepthFormat(framebuffer) {
      let type, format, internalFormat;
      const gl = this.GL;

      if (framebuffer.useStencil) {
        if (framebuffer.depthFormat === FLOAT) {
          type = gl.FLOAT_32_UNSIGNED_INT_24_8_REV;
        } else if (this.webglVersion === WEBGL2) {
          type = gl.UNSIGNED_INT_24_8;
        } else {
          type = gl.getExtension('WEBGL_depth_texture').UNSIGNED_INT_24_8_WEBGL;
        }
      } else {
        if (framebuffer.depthFormat === FLOAT) {
          type = gl.FLOAT;
        } else {
          type = gl.UNSIGNED_INT;
        }
      }

      if (framebuffer.useStencil) {
        format = gl.DEPTH_STENCIL;
      } else {
        format = gl.DEPTH_COMPONENT;
      }

      if (framebuffer.useStencil) {
        if (framebuffer.depthFormat === FLOAT) {
          internalFormat = gl.DEPTH32F_STENCIL8;
        } else if (this.webglVersion === WEBGL2) {
          internalFormat = gl.DEPTH24_STENCIL8;
        } else {
          internalFormat = gl.DEPTH_STENCIL;
        }
      } else if (this.webglVersion === WEBGL2) {
        if (framebuffer.depthFormat === FLOAT) {
          internalFormat = gl.DEPTH_COMPONENT32F;
        } else {
          internalFormat = gl.DEPTH_COMPONENT24;
        }
      } else {
        internalFormat = gl.DEPTH_COMPONENT;
      }

      return { internalFormat, format, type };
    }

    _deleteFramebufferTexture(texture) {
      const gl = this.GL;
      gl.deleteTexture(texture.rawTexture().texture);
      this.textures.delete(texture);
    }

    deleteFramebufferTextures(framebuffer) {
      this._deleteFramebufferTexture(framebuffer.color);
      if (framebuffer.depth) this._deleteFramebufferTexture(framebuffer.depth);
      const gl = this.GL;
      if (framebuffer.colorRenderbuffer) gl.deleteRenderbuffer(framebuffer.colorRenderbuffer);
      if (framebuffer.depthRenderbuffer) gl.deleteRenderbuffer(framebuffer.depthRenderbuffer);
    }

    deleteFramebufferResources(framebuffer) {
      const gl = this.GL;
      gl.deleteFramebuffer(framebuffer.framebuffer);
      if (framebuffer.aaFramebuffer) {
        gl.deleteFramebuffer(framebuffer.aaFramebuffer);
      }
      if (framebuffer.depthRenderbuffer) {
        gl.deleteRenderbuffer(framebuffer.depthRenderbuffer);
      }
      if (framebuffer.colorRenderbuffer) {
        gl.deleteRenderbuffer(framebuffer.colorRenderbuffer);
      }
    }

    getFramebufferToBind(framebuffer) {
      if (framebuffer.antialias) {
        return framebuffer.aaFramebuffer;
      } else {
        return framebuffer.framebuffer;
      }
    }

    updateFramebufferTexture(framebuffer, property) {
      if (framebuffer.antialias) {
        const gl = this.GL;
        gl.bindFramebuffer(gl.READ_FRAMEBUFFER, framebuffer.aaFramebuffer);
        gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, framebuffer.framebuffer);
        const partsToCopy = {
          colorTexture: [
            gl.COLOR_BUFFER_BIT,
            framebuffer.colorP5Texture.magFilter === LINEAR ? gl.LINEAR : gl.NEAREST
          ],
        };
        if (framebuffer.useDepth) {
          partsToCopy.depthTexture = [
            gl.DEPTH_BUFFER_BIT,
            framebuffer.depthP5Texture.magFilter === LINEAR ? gl.LINEAR : gl.NEAREST
          ];
        }
        const [flag, filter] = partsToCopy[property];
        gl.blitFramebuffer(
          0,
          0,
          framebuffer.width * framebuffer.density,
          framebuffer.height * framebuffer.density,
          0,
          0,
          framebuffer.width * framebuffer.density,
          framebuffer.height * framebuffer.density,
          flag,
          filter
        );

        const activeFbo = this.activeFramebuffer();
        this.bindFramebuffer(activeFbo);
      }
    }

    bindFramebuffer(framebuffer) {
      const gl = this.GL;
      gl.bindFramebuffer(
        gl.FRAMEBUFFER,
        framebuffer
          ? this.getFramebufferToBind(framebuffer)
          : null
      );
    }

    framebufferYScale() {
      // WebGL textures are upside-down compared to textures that come from
      // images and graphics. Framebuffer cameras need to invert their y
      // axes when being rendered to so that the texture comes out rightway up
      // when read in shaders or image().
      return -1;
    }

    readFramebufferPixels(framebuffer) {
      const gl = this.GL;
      const prevFramebuffer = this.activeFramebuffer();
      gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer.framebuffer);
      const colorFormat = this._getFramebufferColorFormat(framebuffer);
      const pixels = readPixelsWebGL(
        framebuffer.pixels,
        gl,
        framebuffer.framebuffer,
        0,
        0,
        framebuffer.width * framebuffer.density,
        framebuffer.height * framebuffer.density,
        colorFormat.format,
        colorFormat.type
      );
      this.bindFramebuffer(prevFramebuffer);
      return pixels;
    }

    readFramebufferPixel(framebuffer, x, y) {
      const colorFormat = this._getFramebufferColorFormat(framebuffer);
      return readPixelWebGL(
        this.GL,
        framebuffer.framebuffer,
        x,
        y,
        colorFormat.format,
        colorFormat.type
      );
    }

    readFramebufferRegion(framebuffer, x, y, w, h) {
      const gl = this.GL;
      const colorFormat = this._getFramebufferColorFormat(framebuffer);

      const rawData = readPixelsWebGL(
        undefined,
        gl,
        framebuffer.framebuffer,
        x * framebuffer.density,
        y * framebuffer.density,
        w * framebuffer.density,
        h * framebuffer.density,
        colorFormat.format,
        colorFormat.type
      );

      // Framebuffer data might be either a Uint8Array or Float32Array
      // depending on its format, and it may or may not have an alpha channel.
      // To turn it into an image, we have to normalize the data into a
      // Uint8ClampedArray with alpha.
      const fullData = new Uint8ClampedArray(
        w * h * framebuffer.density * framebuffer.density * 4
      );
      // Default channels that aren't in the framebuffer (e.g. alpha, if the
      // framebuffer is in RGB mode instead of RGBA) to 255
      fullData.fill(255);

      const channels = colorFormat.format === gl.RGB ? 3 : 4;
      for (let yPos = 0; yPos < h * framebuffer.density; yPos++) {
        for (let xPos = 0; xPos < w * framebuffer.density; xPos++) {
          for (let channel = 0; channel < 4; channel++) {
            const idx = (yPos * w * framebuffer.density + xPos) * 4 + channel;
            if (channel < channels) {
              // Find the index of this pixel in `rawData`, which might have a
              // different number of channels
              const rawDataIdx = channels === 4
                ? idx
                : (yPos * w * framebuffer.density + xPos) * channels + channel;
              fullData[idx] = rawData[rawDataIdx];
            }
          }
        }
      }

      // Create image from data
      const region = new Image(w * framebuffer.density, h * framebuffer.density);
      region.imageData = region.canvas.getContext('2d').createImageData(
        region.width,
        region.height
      );
      region.imageData.data.set(fullData);
      region.pixels = region.imageData.data;
      region.updatePixels();
      if (framebuffer.density !== 1) {
        region.pixelDensity(framebuffer.density);
      }
      return region;
    }

    updateFramebufferPixels(framebuffer) {
      const gl = this.GL;
      framebuffer.colorP5Texture.bindTexture();
      const colorFormat = this._getFramebufferColorFormat(framebuffer);

      const channels = colorFormat.format === gl.RGBA ? 4 : 3;
      const len = framebuffer.width * framebuffer.height * framebuffer.density * framebuffer.density * channels;
      const TypedArrayClass = colorFormat.type === gl.UNSIGNED_BYTE ? Uint8Array : Float32Array;

      if (!(framebuffer.pixels instanceof TypedArrayClass) || framebuffer.pixels.length !== len) {
        throw new Error(
          'The pixels array has not been set correctly. Please call loadPixels() before updatePixels().'
        );
      }

      gl.texImage2D(
        gl.TEXTURE_2D,
        0,
        colorFormat.internalFormat,
        framebuffer.width * framebuffer.density,
        framebuffer.height * framebuffer.density,
        0,
        colorFormat.format,
        colorFormat.type,
        framebuffer.pixels
      );
      framebuffer.colorP5Texture.unbindTexture();
      framebuffer.dirty.colorTexture = false;

      const prevFramebuffer = this.activeFramebuffer();
      if (framebuffer.antialias) {
        // We need to make sure the antialiased framebuffer also has the updated
        // pixels so that if more is drawn to it, it goes on top of the updated
        // pixels instead of replacing them.
        // We can't blit the framebuffer to the multisampled antialias
        // framebuffer to leave both in the same state, so instead we have
        // to use image() to put the framebuffer texture onto the antialiased
        // framebuffer.
        framebuffer.begin();
        this.push();
        this.states.setValue('imageMode', CORNER);
        this.setCamera(framebuffer.filterCamera);
        this.resetMatrix();
        this.states.setValue('strokeColor', null);
        this.clear();
        this._drawingFilter = true;
        this.image(
          framebuffer,
          0, 0,
          framebuffer.width, framebuffer.height,
          -this.width / 2, -this.height / 2,
          this.width, this.height
        );
        this._drawingFilter = false;
        this.pop();
        if (framebuffer.useDepth) {
          gl.clearDepth(1);
          gl.clear(gl.DEPTH_BUFFER_BIT);
        }
        framebuffer.end();
      } else {
        gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer.framebuffer);
        if (framebuffer.useDepth) {
          gl.clearDepth(1);
          gl.clear(gl.DEPTH_BUFFER_BIT);
        }
        this.bindFramebuffer(prevFramebuffer);
      }
    }

    getNoiseShaderSnippet() {
      return noiseGLSL;
    }

  }

  function rendererGL(p5, fn) {
    p5.RendererGL = RendererGL;

    /**
     * @module Rendering
     * @submodule Rendering
     * @for p5
     */
    /**
     * Set attributes for the WebGL Drawing context.
     * This is a way of adjusting how the WebGL
     * renderer works to fine-tune the display and performance.
     *
     * Note that this will reinitialize the drawing context
     * if called after the WebGL canvas is made.
     *
     * If an object is passed as the parameter, all attributes
     * not declared in the object will be set to defaults.
     *
     * The available attributes are:
     * <br>
     * alpha - indicates if the canvas contains an alpha buffer
     * default is true
     *
     * depth - indicates whether the drawing buffer has a depth buffer
     * of at least 16 bits - default is true
     *
     * stencil - indicates whether the drawing buffer has a stencil buffer
     * of at least 8 bits
     *
     * antialias - indicates whether or not to perform anti-aliasing
     * default is false (true in Safari)
     *
     * premultipliedAlpha - indicates that the page compositor will assume
     * the drawing buffer contains colors with pre-multiplied alpha
     * default is true
     *
     * preserveDrawingBuffer - if true the buffers will not be cleared and
     * and will preserve their values until cleared or overwritten by author
     * (note that p5 clears automatically on draw loop)
     * default is true
     *
     * perPixelLighting - if true, per-pixel lighting will be used in the
     * lighting shader otherwise per-vertex lighting is used.
     * default is true.
     *
     * version - either 1 or 2, to specify which WebGL version to ask for. By
     * default, WebGL 2 will be requested. If WebGL2 is not available, it will
     * fall back to WebGL 1. You can check what version is used with by looking at
     * the global `webglVersion` property.
     *
     * @method setAttributes
     * @for p5
     * @param  {String}  key Name of attribute
     * @param  {Boolean}        value New value of named attribute
     * @example
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     * }
     *
     * function draw() {
     *   background(255);
     *   push();
     *   rotateZ(frameCount * 0.02);
     *   rotateX(frameCount * 0.02);
     *   rotateY(frameCount * 0.02);
     *   fill(0, 0, 0);
     *   box(50);
     *   pop();
     * }
     *
     * @example
     *  // Now with the antialias attribute set to true.
     * function setup() {
     *   setAttributes('antialias', true);
     *   createCanvas(100, 100, WEBGL);
     * }
     *
     * function draw() {
     *   background(255);
     *   push();
     *   rotateZ(frameCount * 0.02);
     *   rotateX(frameCount * 0.02);
     *   rotateY(frameCount * 0.02);
     *   fill(0, 0, 0);
     *   box(50);
     *   pop();
     * }
     *
     * @example
     * // press the mouse button to disable perPixelLighting
     * function setup() {
     *   createCanvas(100, 100, WEBGL);
     *   noStroke();
     *   fill(255);
     * }
     *
     * let lights = [
     *   { c: '#f00', t: 1.12, p: 1.91, r: 0.2 },
     *   { c: '#0f0', t: 1.21, p: 1.31, r: 0.2 },
     *   { c: '#00f', t: 1.37, p: 1.57, r: 0.2 },
     *   { c: '#ff0', t: 1.12, p: 1.91, r: 0.7 },
     *   { c: '#0ff', t: 1.21, p: 1.31, r: 0.7 },
     *   { c: '#f0f', t: 1.37, p: 1.57, r: 0.7 }
     * ];
     *
     * function draw() {
     *   let t = millis() / 1000 + 1000;
     *   background(0);
     *   directionalLight(color('#222'), 1, 1, 1);
     *
     *   for (let i = 0; i < lights.length; i++) {
     *     let light = lights[i];
     *     pointLight(
     *       color(light.c),
     *       p5.Vector.fromAngles(t * light.t, t * light.p, width * light.r)
     *     );
     *   }
     *
     *   specularMaterial(255);
     *   sphere(width * 0.1);
     *
     *   rotateX(t * 0.77);
     *   rotateY(t * 0.83);
     *   rotateZ(t * 0.91);
     *   torus(width * 0.3, width * 0.07, 24, 10);
     * }
     *
     * function mousePressed() {
     *   setAttributes('perPixelLighting', false);
     *   noStroke();
     *   fill(255);
     * }
     * function mouseReleased() {
     *   setAttributes('perPixelLighting', true);
     *   noStroke();
     *   fill(255);
     * }
     *
     * @alt a rotating cube with smoother edges
     */
    /**
     * @method setAttributes
     * @for p5
     * @param  {Object}  obj object with key-value pairs
     */
    fn.setAttributes = function (key, value) {
      return this._renderer._setAttributes(key, value);
    };

    /**
     * ensures that p5 is using a 3d renderer. throws an error if not.
     */
    fn._assert3d = function (name) {
      if (!this._renderer.isP3D)
        throw new Error(
          `${name}() is only supported in WEBGL mode. If you'd like to use 3D graphics and WebGL, see  https://p5js.org/examples/form-3d-primitives.html for more information.`
        );
    };

    p5.renderers[WEBGL] = p5.RendererGL;
    p5.renderers[WEBGL2] = p5.RendererGL;
  }

  if (typeof p5 !== "undefined") {
    rendererGL(p5, p5.prototype);
  }

  function webgl(p5){
    p5.registerAddon(renderer3D);
    p5.registerAddon(rendererGL);
    p5.registerAddon(primitives3D);
    p5.registerAddon(interaction);
    p5.registerAddon(light);
    p5.registerAddon(loading$1);
    p5.registerAddon(material);
    p5.registerAddon(text);
    p5.registerAddon(renderBuffer);
    p5.registerAddon(quat);
    p5.registerAddon(matrix);
    p5.registerAddon(geometry);
    p5.registerAddon(camera);
    p5.registerAddon(framebuffer);
    p5.registerAddon(dataArray);
    p5.registerAddon(texture);
  }

  function type(p5){
    p5.registerAddon(textCore);
    p5.registerAddon(font);
  }

  var escodegen$1 = {};

  var estraverse = {};

  /*
    Copyright (C) 2012-2013 Yusuke Suzuki <utatane.tea@gmail.com>
    Copyright (C) 2012 Ariya Hidayat <ariya.hidayat@gmail.com>

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:

      * Redistributions of source code must retain the above copyright
        notice, this list of conditions and the following disclaimer.
      * Redistributions in binary form must reproduce the above copyright
        notice, this list of conditions and the following disclaimer in the
        documentation and/or other materials provided with the distribution.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
    DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
    ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
    THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */

  (function (exports$1) {
  	/*jslint vars:false, bitwise:true*/
  	/*jshint indent:4*/
  	/*global exports:true*/
  	(function clone(exports$1) {

  	    var Syntax,
  	        VisitorOption,
  	        VisitorKeys,
  	        BREAK,
  	        SKIP,
  	        REMOVE;

  	    function deepCopy(obj) {
  	        var ret = {}, key, val;
  	        for (key in obj) {
  	            if (obj.hasOwnProperty(key)) {
  	                val = obj[key];
  	                if (typeof val === 'object' && val !== null) {
  	                    ret[key] = deepCopy(val);
  	                } else {
  	                    ret[key] = val;
  	                }
  	            }
  	        }
  	        return ret;
  	    }

  	    // based on LLVM libc++ upper_bound / lower_bound
  	    // MIT License

  	    function upperBound(array, func) {
  	        var diff, len, i, current;

  	        len = array.length;
  	        i = 0;

  	        while (len) {
  	            diff = len >>> 1;
  	            current = i + diff;
  	            if (func(array[current])) {
  	                len = diff;
  	            } else {
  	                i = current + 1;
  	                len -= diff + 1;
  	            }
  	        }
  	        return i;
  	    }

  	    Syntax = {
  	        AssignmentExpression: 'AssignmentExpression',
  	        AssignmentPattern: 'AssignmentPattern',
  	        ArrayExpression: 'ArrayExpression',
  	        ArrayPattern: 'ArrayPattern',
  	        ArrowFunctionExpression: 'ArrowFunctionExpression',
  	        AwaitExpression: 'AwaitExpression', // CAUTION: It's deferred to ES7.
  	        BlockStatement: 'BlockStatement',
  	        BinaryExpression: 'BinaryExpression',
  	        BreakStatement: 'BreakStatement',
  	        CallExpression: 'CallExpression',
  	        CatchClause: 'CatchClause',
  	        ChainExpression: 'ChainExpression',
  	        ClassBody: 'ClassBody',
  	        ClassDeclaration: 'ClassDeclaration',
  	        ClassExpression: 'ClassExpression',
  	        ComprehensionBlock: 'ComprehensionBlock',  // CAUTION: It's deferred to ES7.
  	        ComprehensionExpression: 'ComprehensionExpression',  // CAUTION: It's deferred to ES7.
  	        ConditionalExpression: 'ConditionalExpression',
  	        ContinueStatement: 'ContinueStatement',
  	        DebuggerStatement: 'DebuggerStatement',
  	        DirectiveStatement: 'DirectiveStatement',
  	        DoWhileStatement: 'DoWhileStatement',
  	        EmptyStatement: 'EmptyStatement',
  	        ExportAllDeclaration: 'ExportAllDeclaration',
  	        ExportDefaultDeclaration: 'ExportDefaultDeclaration',
  	        ExportNamedDeclaration: 'ExportNamedDeclaration',
  	        ExportSpecifier: 'ExportSpecifier',
  	        ExpressionStatement: 'ExpressionStatement',
  	        ForStatement: 'ForStatement',
  	        ForInStatement: 'ForInStatement',
  	        ForOfStatement: 'ForOfStatement',
  	        FunctionDeclaration: 'FunctionDeclaration',
  	        FunctionExpression: 'FunctionExpression',
  	        GeneratorExpression: 'GeneratorExpression',  // CAUTION: It's deferred to ES7.
  	        Identifier: 'Identifier',
  	        IfStatement: 'IfStatement',
  	        ImportExpression: 'ImportExpression',
  	        ImportDeclaration: 'ImportDeclaration',
  	        ImportDefaultSpecifier: 'ImportDefaultSpecifier',
  	        ImportNamespaceSpecifier: 'ImportNamespaceSpecifier',
  	        ImportSpecifier: 'ImportSpecifier',
  	        Literal: 'Literal',
  	        LabeledStatement: 'LabeledStatement',
  	        LogicalExpression: 'LogicalExpression',
  	        MemberExpression: 'MemberExpression',
  	        MetaProperty: 'MetaProperty',
  	        MethodDefinition: 'MethodDefinition',
  	        ModuleSpecifier: 'ModuleSpecifier',
  	        NewExpression: 'NewExpression',
  	        ObjectExpression: 'ObjectExpression',
  	        ObjectPattern: 'ObjectPattern',
  	        PrivateIdentifier: 'PrivateIdentifier',
  	        Program: 'Program',
  	        Property: 'Property',
  	        PropertyDefinition: 'PropertyDefinition',
  	        RestElement: 'RestElement',
  	        ReturnStatement: 'ReturnStatement',
  	        SequenceExpression: 'SequenceExpression',
  	        SpreadElement: 'SpreadElement',
  	        Super: 'Super',
  	        SwitchStatement: 'SwitchStatement',
  	        SwitchCase: 'SwitchCase',
  	        TaggedTemplateExpression: 'TaggedTemplateExpression',
  	        TemplateElement: 'TemplateElement',
  	        TemplateLiteral: 'TemplateLiteral',
  	        ThisExpression: 'ThisExpression',
  	        ThrowStatement: 'ThrowStatement',
  	        TryStatement: 'TryStatement',
  	        UnaryExpression: 'UnaryExpression',
  	        UpdateExpression: 'UpdateExpression',
  	        VariableDeclaration: 'VariableDeclaration',
  	        VariableDeclarator: 'VariableDeclarator',
  	        WhileStatement: 'WhileStatement',
  	        WithStatement: 'WithStatement',
  	        YieldExpression: 'YieldExpression'
  	    };

  	    VisitorKeys = {
  	        AssignmentExpression: ['left', 'right'],
  	        AssignmentPattern: ['left', 'right'],
  	        ArrayExpression: ['elements'],
  	        ArrayPattern: ['elements'],
  	        ArrowFunctionExpression: ['params', 'body'],
  	        AwaitExpression: ['argument'], // CAUTION: It's deferred to ES7.
  	        BlockStatement: ['body'],
  	        BinaryExpression: ['left', 'right'],
  	        BreakStatement: ['label'],
  	        CallExpression: ['callee', 'arguments'],
  	        CatchClause: ['param', 'body'],
  	        ChainExpression: ['expression'],
  	        ClassBody: ['body'],
  	        ClassDeclaration: ['id', 'superClass', 'body'],
  	        ClassExpression: ['id', 'superClass', 'body'],
  	        ComprehensionBlock: ['left', 'right'],  // CAUTION: It's deferred to ES7.
  	        ComprehensionExpression: ['blocks', 'filter', 'body'],  // CAUTION: It's deferred to ES7.
  	        ConditionalExpression: ['test', 'consequent', 'alternate'],
  	        ContinueStatement: ['label'],
  	        DebuggerStatement: [],
  	        DirectiveStatement: [],
  	        DoWhileStatement: ['body', 'test'],
  	        EmptyStatement: [],
  	        ExportAllDeclaration: ['source'],
  	        ExportDefaultDeclaration: ['declaration'],
  	        ExportNamedDeclaration: ['declaration', 'specifiers', 'source'],
  	        ExportSpecifier: ['exported', 'local'],
  	        ExpressionStatement: ['expression'],
  	        ForStatement: ['init', 'test', 'update', 'body'],
  	        ForInStatement: ['left', 'right', 'body'],
  	        ForOfStatement: ['left', 'right', 'body'],
  	        FunctionDeclaration: ['id', 'params', 'body'],
  	        FunctionExpression: ['id', 'params', 'body'],
  	        GeneratorExpression: ['blocks', 'filter', 'body'],  // CAUTION: It's deferred to ES7.
  	        Identifier: [],
  	        IfStatement: ['test', 'consequent', 'alternate'],
  	        ImportExpression: ['source'],
  	        ImportDeclaration: ['specifiers', 'source'],
  	        ImportDefaultSpecifier: ['local'],
  	        ImportNamespaceSpecifier: ['local'],
  	        ImportSpecifier: ['imported', 'local'],
  	        Literal: [],
  	        LabeledStatement: ['label', 'body'],
  	        LogicalExpression: ['left', 'right'],
  	        MemberExpression: ['object', 'property'],
  	        MetaProperty: ['meta', 'property'],
  	        MethodDefinition: ['key', 'value'],
  	        ModuleSpecifier: [],
  	        NewExpression: ['callee', 'arguments'],
  	        ObjectExpression: ['properties'],
  	        ObjectPattern: ['properties'],
  	        PrivateIdentifier: [],
  	        Program: ['body'],
  	        Property: ['key', 'value'],
  	        PropertyDefinition: ['key', 'value'],
  	        RestElement: [ 'argument' ],
  	        ReturnStatement: ['argument'],
  	        SequenceExpression: ['expressions'],
  	        SpreadElement: ['argument'],
  	        Super: [],
  	        SwitchStatement: ['discriminant', 'cases'],
  	        SwitchCase: ['test', 'consequent'],
  	        TaggedTemplateExpression: ['tag', 'quasi'],
  	        TemplateElement: [],
  	        TemplateLiteral: ['quasis', 'expressions'],
  	        ThisExpression: [],
  	        ThrowStatement: ['argument'],
  	        TryStatement: ['block', 'handler', 'finalizer'],
  	        UnaryExpression: ['argument'],
  	        UpdateExpression: ['argument'],
  	        VariableDeclaration: ['declarations'],
  	        VariableDeclarator: ['id', 'init'],
  	        WhileStatement: ['test', 'body'],
  	        WithStatement: ['object', 'body'],
  	        YieldExpression: ['argument']
  	    };

  	    // unique id
  	    BREAK = {};
  	    SKIP = {};
  	    REMOVE = {};

  	    VisitorOption = {
  	        Break: BREAK,
  	        Skip: SKIP,
  	        Remove: REMOVE
  	    };

  	    function Reference(parent, key) {
  	        this.parent = parent;
  	        this.key = key;
  	    }

  	    Reference.prototype.replace = function replace(node) {
  	        this.parent[this.key] = node;
  	    };

  	    Reference.prototype.remove = function remove() {
  	        if (Array.isArray(this.parent)) {
  	            this.parent.splice(this.key, 1);
  	            return true;
  	        } else {
  	            this.replace(null);
  	            return false;
  	        }
  	    };

  	    function Element(node, path, wrap, ref) {
  	        this.node = node;
  	        this.path = path;
  	        this.wrap = wrap;
  	        this.ref = ref;
  	    }

  	    function Controller() { }

  	    // API:
  	    // return property path array from root to current node
  	    Controller.prototype.path = function path() {
  	        var i, iz, j, jz, result, element;

  	        function addToPath(result, path) {
  	            if (Array.isArray(path)) {
  	                for (j = 0, jz = path.length; j < jz; ++j) {
  	                    result.push(path[j]);
  	                }
  	            } else {
  	                result.push(path);
  	            }
  	        }

  	        // root node
  	        if (!this.__current.path) {
  	            return null;
  	        }

  	        // first node is sentinel, second node is root element
  	        result = [];
  	        for (i = 2, iz = this.__leavelist.length; i < iz; ++i) {
  	            element = this.__leavelist[i];
  	            addToPath(result, element.path);
  	        }
  	        addToPath(result, this.__current.path);
  	        return result;
  	    };

  	    // API:
  	    // return type of current node
  	    Controller.prototype.type = function () {
  	        var node = this.current();
  	        return node.type || this.__current.wrap;
  	    };

  	    // API:
  	    // return array of parent elements
  	    Controller.prototype.parents = function parents() {
  	        var i, iz, result;

  	        // first node is sentinel
  	        result = [];
  	        for (i = 1, iz = this.__leavelist.length; i < iz; ++i) {
  	            result.push(this.__leavelist[i].node);
  	        }

  	        return result;
  	    };

  	    // API:
  	    // return current node
  	    Controller.prototype.current = function current() {
  	        return this.__current.node;
  	    };

  	    Controller.prototype.__execute = function __execute(callback, element) {
  	        var previous, result;

  	        result = undefined;

  	        previous  = this.__current;
  	        this.__current = element;
  	        this.__state = null;
  	        if (callback) {
  	            result = callback.call(this, element.node, this.__leavelist[this.__leavelist.length - 1].node);
  	        }
  	        this.__current = previous;

  	        return result;
  	    };

  	    // API:
  	    // notify control skip / break
  	    Controller.prototype.notify = function notify(flag) {
  	        this.__state = flag;
  	    };

  	    // API:
  	    // skip child nodes of current node
  	    Controller.prototype.skip = function () {
  	        this.notify(SKIP);
  	    };

  	    // API:
  	    // break traversals
  	    Controller.prototype['break'] = function () {
  	        this.notify(BREAK);
  	    };

  	    // API:
  	    // remove node
  	    Controller.prototype.remove = function () {
  	        this.notify(REMOVE);
  	    };

  	    Controller.prototype.__initialize = function(root, visitor) {
  	        this.visitor = visitor;
  	        this.root = root;
  	        this.__worklist = [];
  	        this.__leavelist = [];
  	        this.__current = null;
  	        this.__state = null;
  	        this.__fallback = null;
  	        if (visitor.fallback === 'iteration') {
  	            this.__fallback = Object.keys;
  	        } else if (typeof visitor.fallback === 'function') {
  	            this.__fallback = visitor.fallback;
  	        }

  	        this.__keys = VisitorKeys;
  	        if (visitor.keys) {
  	            this.__keys = Object.assign(Object.create(this.__keys), visitor.keys);
  	        }
  	    };

  	    function isNode(node) {
  	        if (node == null) {
  	            return false;
  	        }
  	        return typeof node === 'object' && typeof node.type === 'string';
  	    }

  	    function isProperty(nodeType, key) {
  	        return (nodeType === Syntax.ObjectExpression || nodeType === Syntax.ObjectPattern) && 'properties' === key;
  	    }
  	  
  	    function candidateExistsInLeaveList(leavelist, candidate) {
  	        for (var i = leavelist.length - 1; i >= 0; --i) {
  	            if (leavelist[i].node === candidate) {
  	                return true;
  	            }
  	        }
  	        return false;
  	    }

  	    Controller.prototype.traverse = function traverse(root, visitor) {
  	        var worklist,
  	            leavelist,
  	            element,
  	            node,
  	            nodeType,
  	            ret,
  	            key,
  	            current,
  	            current2,
  	            candidates,
  	            candidate,
  	            sentinel;

  	        this.__initialize(root, visitor);

  	        sentinel = {};

  	        // reference
  	        worklist = this.__worklist;
  	        leavelist = this.__leavelist;

  	        // initialize
  	        worklist.push(new Element(root, null, null, null));
  	        leavelist.push(new Element(null, null, null, null));

  	        while (worklist.length) {
  	            element = worklist.pop();

  	            if (element === sentinel) {
  	                element = leavelist.pop();

  	                ret = this.__execute(visitor.leave, element);

  	                if (this.__state === BREAK || ret === BREAK) {
  	                    return;
  	                }
  	                continue;
  	            }

  	            if (element.node) {

  	                ret = this.__execute(visitor.enter, element);

  	                if (this.__state === BREAK || ret === BREAK) {
  	                    return;
  	                }

  	                worklist.push(sentinel);
  	                leavelist.push(element);

  	                if (this.__state === SKIP || ret === SKIP) {
  	                    continue;
  	                }

  	                node = element.node;
  	                nodeType = node.type || element.wrap;
  	                candidates = this.__keys[nodeType];
  	                if (!candidates) {
  	                    if (this.__fallback) {
  	                        candidates = this.__fallback(node);
  	                    } else {
  	                        throw new Error('Unknown node type ' + nodeType + '.');
  	                    }
  	                }

  	                current = candidates.length;
  	                while ((current -= 1) >= 0) {
  	                    key = candidates[current];
  	                    candidate = node[key];
  	                    if (!candidate) {
  	                        continue;
  	                    }

  	                    if (Array.isArray(candidate)) {
  	                        current2 = candidate.length;
  	                        while ((current2 -= 1) >= 0) {
  	                            if (!candidate[current2]) {
  	                                continue;
  	                            }

  	                            if (candidateExistsInLeaveList(leavelist, candidate[current2])) {
  	                              continue;
  	                            }

  	                            if (isProperty(nodeType, candidates[current])) {
  	                                element = new Element(candidate[current2], [key, current2], 'Property', null);
  	                            } else if (isNode(candidate[current2])) {
  	                                element = new Element(candidate[current2], [key, current2], null, null);
  	                            } else {
  	                                continue;
  	                            }
  	                            worklist.push(element);
  	                        }
  	                    } else if (isNode(candidate)) {
  	                        if (candidateExistsInLeaveList(leavelist, candidate)) {
  	                          continue;
  	                        }

  	                        worklist.push(new Element(candidate, key, null, null));
  	                    }
  	                }
  	            }
  	        }
  	    };

  	    Controller.prototype.replace = function replace(root, visitor) {
  	        var worklist,
  	            leavelist,
  	            node,
  	            nodeType,
  	            target,
  	            element,
  	            current,
  	            current2,
  	            candidates,
  	            candidate,
  	            sentinel,
  	            outer,
  	            key;

  	        function removeElem(element) {
  	            var i,
  	                key,
  	                nextElem,
  	                parent;

  	            if (element.ref.remove()) {
  	                // When the reference is an element of an array.
  	                key = element.ref.key;
  	                parent = element.ref.parent;

  	                // If removed from array, then decrease following items' keys.
  	                i = worklist.length;
  	                while (i--) {
  	                    nextElem = worklist[i];
  	                    if (nextElem.ref && nextElem.ref.parent === parent) {
  	                        if  (nextElem.ref.key < key) {
  	                            break;
  	                        }
  	                        --nextElem.ref.key;
  	                    }
  	                }
  	            }
  	        }

  	        this.__initialize(root, visitor);

  	        sentinel = {};

  	        // reference
  	        worklist = this.__worklist;
  	        leavelist = this.__leavelist;

  	        // initialize
  	        outer = {
  	            root: root
  	        };
  	        element = new Element(root, null, null, new Reference(outer, 'root'));
  	        worklist.push(element);
  	        leavelist.push(element);

  	        while (worklist.length) {
  	            element = worklist.pop();

  	            if (element === sentinel) {
  	                element = leavelist.pop();

  	                target = this.__execute(visitor.leave, element);

  	                // node may be replaced with null,
  	                // so distinguish between undefined and null in this place
  	                if (target !== undefined && target !== BREAK && target !== SKIP && target !== REMOVE) {
  	                    // replace
  	                    element.ref.replace(target);
  	                }

  	                if (this.__state === REMOVE || target === REMOVE) {
  	                    removeElem(element);
  	                }

  	                if (this.__state === BREAK || target === BREAK) {
  	                    return outer.root;
  	                }
  	                continue;
  	            }

  	            target = this.__execute(visitor.enter, element);

  	            // node may be replaced with null,
  	            // so distinguish between undefined and null in this place
  	            if (target !== undefined && target !== BREAK && target !== SKIP && target !== REMOVE) {
  	                // replace
  	                element.ref.replace(target);
  	                element.node = target;
  	            }

  	            if (this.__state === REMOVE || target === REMOVE) {
  	                removeElem(element);
  	                element.node = null;
  	            }

  	            if (this.__state === BREAK || target === BREAK) {
  	                return outer.root;
  	            }

  	            // node may be null
  	            node = element.node;
  	            if (!node) {
  	                continue;
  	            }

  	            worklist.push(sentinel);
  	            leavelist.push(element);

  	            if (this.__state === SKIP || target === SKIP) {
  	                continue;
  	            }

  	            nodeType = node.type || element.wrap;
  	            candidates = this.__keys[nodeType];
  	            if (!candidates) {
  	                if (this.__fallback) {
  	                    candidates = this.__fallback(node);
  	                } else {
  	                    throw new Error('Unknown node type ' + nodeType + '.');
  	                }
  	            }

  	            current = candidates.length;
  	            while ((current -= 1) >= 0) {
  	                key = candidates[current];
  	                candidate = node[key];
  	                if (!candidate) {
  	                    continue;
  	                }

  	                if (Array.isArray(candidate)) {
  	                    current2 = candidate.length;
  	                    while ((current2 -= 1) >= 0) {
  	                        if (!candidate[current2]) {
  	                            continue;
  	                        }
  	                        if (isProperty(nodeType, candidates[current])) {
  	                            element = new Element(candidate[current2], [key, current2], 'Property', new Reference(candidate, current2));
  	                        } else if (isNode(candidate[current2])) {
  	                            element = new Element(candidate[current2], [key, current2], null, new Reference(candidate, current2));
  	                        } else {
  	                            continue;
  	                        }
  	                        worklist.push(element);
  	                    }
  	                } else if (isNode(candidate)) {
  	                    worklist.push(new Element(candidate, key, null, new Reference(node, key)));
  	                }
  	            }
  	        }

  	        return outer.root;
  	    };

  	    function traverse(root, visitor) {
  	        var controller = new Controller();
  	        return controller.traverse(root, visitor);
  	    }

  	    function replace(root, visitor) {
  	        var controller = new Controller();
  	        return controller.replace(root, visitor);
  	    }

  	    function extendCommentRange(comment, tokens) {
  	        var target;

  	        target = upperBound(tokens, function search(token) {
  	            return token.range[0] > comment.range[0];
  	        });

  	        comment.extendedRange = [comment.range[0], comment.range[1]];

  	        if (target !== tokens.length) {
  	            comment.extendedRange[1] = tokens[target].range[0];
  	        }

  	        target -= 1;
  	        if (target >= 0) {
  	            comment.extendedRange[0] = tokens[target].range[1];
  	        }

  	        return comment;
  	    }

  	    function attachComments(tree, providedComments, tokens) {
  	        // At first, we should calculate extended comment ranges.
  	        var comments = [], comment, len, i, cursor;

  	        if (!tree.range) {
  	            throw new Error('attachComments needs range information');
  	        }

  	        // tokens array is empty, we attach comments to tree as 'leadingComments'
  	        if (!tokens.length) {
  	            if (providedComments.length) {
  	                for (i = 0, len = providedComments.length; i < len; i += 1) {
  	                    comment = deepCopy(providedComments[i]);
  	                    comment.extendedRange = [0, tree.range[0]];
  	                    comments.push(comment);
  	                }
  	                tree.leadingComments = comments;
  	            }
  	            return tree;
  	        }

  	        for (i = 0, len = providedComments.length; i < len; i += 1) {
  	            comments.push(extendCommentRange(deepCopy(providedComments[i]), tokens));
  	        }

  	        // This is based on John Freeman's implementation.
  	        cursor = 0;
  	        traverse(tree, {
  	            enter: function (node) {
  	                var comment;

  	                while (cursor < comments.length) {
  	                    comment = comments[cursor];
  	                    if (comment.extendedRange[1] > node.range[0]) {
  	                        break;
  	                    }

  	                    if (comment.extendedRange[1] === node.range[0]) {
  	                        if (!node.leadingComments) {
  	                            node.leadingComments = [];
  	                        }
  	                        node.leadingComments.push(comment);
  	                        comments.splice(cursor, 1);
  	                    } else {
  	                        cursor += 1;
  	                    }
  	                }

  	                // already out of owned node
  	                if (cursor === comments.length) {
  	                    return VisitorOption.Break;
  	                }

  	                if (comments[cursor].extendedRange[0] > node.range[1]) {
  	                    return VisitorOption.Skip;
  	                }
  	            }
  	        });

  	        cursor = 0;
  	        traverse(tree, {
  	            leave: function (node) {
  	                var comment;

  	                while (cursor < comments.length) {
  	                    comment = comments[cursor];
  	                    if (node.range[1] < comment.extendedRange[0]) {
  	                        break;
  	                    }

  	                    if (node.range[1] === comment.extendedRange[0]) {
  	                        if (!node.trailingComments) {
  	                            node.trailingComments = [];
  	                        }
  	                        node.trailingComments.push(comment);
  	                        comments.splice(cursor, 1);
  	                    } else {
  	                        cursor += 1;
  	                    }
  	                }

  	                // already out of owned node
  	                if (cursor === comments.length) {
  	                    return VisitorOption.Break;
  	                }

  	                if (comments[cursor].extendedRange[0] > node.range[1]) {
  	                    return VisitorOption.Skip;
  	                }
  	            }
  	        });

  	        return tree;
  	    }

  	    exports$1.Syntax = Syntax;
  	    exports$1.traverse = traverse;
  	    exports$1.replace = replace;
  	    exports$1.attachComments = attachComments;
  	    exports$1.VisitorKeys = VisitorKeys;
  	    exports$1.VisitorOption = VisitorOption;
  	    exports$1.Controller = Controller;
  	    exports$1.cloneEnvironment = function () { return clone({}); };

  	    return exports$1;
  	}(exports$1));
  	/* vim: set sw=4 ts=4 et tw=80 : */ 
  } (estraverse));

  var utils = {};

  var ast = {exports: {}};

  /*
    Copyright (C) 2013 Yusuke Suzuki <utatane.tea@gmail.com>

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:

      * Redistributions of source code must retain the above copyright
        notice, this list of conditions and the following disclaimer.
      * Redistributions in binary form must reproduce the above copyright
        notice, this list of conditions and the following disclaimer in the
        documentation and/or other materials provided with the distribution.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
    DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
    ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
    THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */

  (function () {

      function isExpression(node) {
          if (node == null) { return false; }
          switch (node.type) {
              case 'ArrayExpression':
              case 'AssignmentExpression':
              case 'BinaryExpression':
              case 'CallExpression':
              case 'ConditionalExpression':
              case 'FunctionExpression':
              case 'Identifier':
              case 'Literal':
              case 'LogicalExpression':
              case 'MemberExpression':
              case 'NewExpression':
              case 'ObjectExpression':
              case 'SequenceExpression':
              case 'ThisExpression':
              case 'UnaryExpression':
              case 'UpdateExpression':
                  return true;
          }
          return false;
      }

      function isIterationStatement(node) {
          if (node == null) { return false; }
          switch (node.type) {
              case 'DoWhileStatement':
              case 'ForInStatement':
              case 'ForStatement':
              case 'WhileStatement':
                  return true;
          }
          return false;
      }

      function isStatement(node) {
          if (node == null) { return false; }
          switch (node.type) {
              case 'BlockStatement':
              case 'BreakStatement':
              case 'ContinueStatement':
              case 'DebuggerStatement':
              case 'DoWhileStatement':
              case 'EmptyStatement':
              case 'ExpressionStatement':
              case 'ForInStatement':
              case 'ForStatement':
              case 'IfStatement':
              case 'LabeledStatement':
              case 'ReturnStatement':
              case 'SwitchStatement':
              case 'ThrowStatement':
              case 'TryStatement':
              case 'VariableDeclaration':
              case 'WhileStatement':
              case 'WithStatement':
                  return true;
          }
          return false;
      }

      function isSourceElement(node) {
        return isStatement(node) || node != null && node.type === 'FunctionDeclaration';
      }

      function trailingStatement(node) {
          switch (node.type) {
          case 'IfStatement':
              if (node.alternate != null) {
                  return node.alternate;
              }
              return node.consequent;

          case 'LabeledStatement':
          case 'ForStatement':
          case 'ForInStatement':
          case 'WhileStatement':
          case 'WithStatement':
              return node.body;
          }
          return null;
      }

      function isProblematicIfStatement(node) {
          var current;

          if (node.type !== 'IfStatement') {
              return false;
          }
          if (node.alternate == null) {
              return false;
          }
          current = node.consequent;
          do {
              if (current.type === 'IfStatement') {
                  if (current.alternate == null)  {
                      return true;
                  }
              }
              current = trailingStatement(current);
          } while (current);

          return false;
      }

      ast.exports = {
          isExpression: isExpression,
          isStatement: isStatement,
          isIterationStatement: isIterationStatement,
          isSourceElement: isSourceElement,
          isProblematicIfStatement: isProblematicIfStatement,

          trailingStatement: trailingStatement
      };
  }());
  /* vim: set sw=4 ts=4 et tw=80 : */

  var astExports = ast.exports;

  var code = {exports: {}};

  /*
    Copyright (C) 2013-2014 Yusuke Suzuki <utatane.tea@gmail.com>
    Copyright (C) 2014 Ivan Nikulin <ifaaan@gmail.com>

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:

      * Redistributions of source code must retain the above copyright
        notice, this list of conditions and the following disclaimer.
      * Redistributions in binary form must reproduce the above copyright
        notice, this list of conditions and the following disclaimer in the
        documentation and/or other materials provided with the distribution.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
    DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
    ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
    THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */

  (function () {

      var ES6Regex, ES5Regex, NON_ASCII_WHITESPACES, IDENTIFIER_START, IDENTIFIER_PART, ch;

      // See `tools/generate-identifier-regex.js`.
      ES5Regex = {
          // ECMAScript 5.1/Unicode v9.0.0 NonAsciiIdentifierStart:
          NonAsciiIdentifierStart: /[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]/,
          // ECMAScript 5.1/Unicode v9.0.0 NonAsciiIdentifierPart:
          NonAsciiIdentifierPart: /[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u08A0-\u08B4\u08B6-\u08BD\u08D4-\u08E1\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C80-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D01-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D54-\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1CD0-\u1CD2\u1CD4-\u1CF6\u1CF8\u1CF9\u1D00-\u1DF5\u1DFB-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200C\u200D\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C5\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]/
      };

      ES6Regex = {
          // ECMAScript 6/Unicode v9.0.0 NonAsciiIdentifierStart:
          NonAsciiIdentifierStart: /[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309B-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF30-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F\uDFE0]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D]/,
          // ECMAScript 6/Unicode v9.0.0 NonAsciiIdentifierPart:
          NonAsciiIdentifierPart: /[\xAA\xB5\xB7\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u08A0-\u08B4\u08B6-\u08BD\u08D4-\u08E1\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C80-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D01-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D54-\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1369-\u1371\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1CD0-\u1CD2\u1CD4-\u1CF6\u1CF8\u1CF9\u1D00-\u1DF5\u1DFB-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200C\u200D\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C5\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF30-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC00-\uDC46\uDC66-\uDC6F\uDC7F-\uDCBA\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDCA-\uDDCC\uDDD0-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE3E\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3C-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC00-\uDC4A\uDC50-\uDC59\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB7\uDEC0-\uDEC9\uDF00-\uDF19\uDF1D-\uDF2B\uDF30-\uDF39]|\uD806[\uDCA0-\uDCE9\uDCFF\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC36\uDC38-\uDC40\uDC50-\uDC59\uDC72-\uDC8F\uDC92-\uDCA7\uDCA9-\uDCB6]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF8F-\uDF9F\uDFE0]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6\uDD00-\uDD4A\uDD50-\uDD59]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D]|\uDB40[\uDD00-\uDDEF]/
      };

      function isDecimalDigit(ch) {
          return 0x30 <= ch && ch <= 0x39;  // 0..9
      }

      function isHexDigit(ch) {
          return 0x30 <= ch && ch <= 0x39 ||  // 0..9
              0x61 <= ch && ch <= 0x66 ||     // a..f
              0x41 <= ch && ch <= 0x46;       // A..F
      }

      function isOctalDigit(ch) {
          return ch >= 0x30 && ch <= 0x37;  // 0..7
      }

      // 7.2 White Space

      NON_ASCII_WHITESPACES = [
          0x1680,
          0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200A,
          0x202F, 0x205F,
          0x3000,
          0xFEFF
      ];

      function isWhiteSpace(ch) {
          return ch === 0x20 || ch === 0x09 || ch === 0x0B || ch === 0x0C || ch === 0xA0 ||
              ch >= 0x1680 && NON_ASCII_WHITESPACES.indexOf(ch) >= 0;
      }

      // 7.3 Line Terminators

      function isLineTerminator(ch) {
          return ch === 0x0A || ch === 0x0D || ch === 0x2028 || ch === 0x2029;
      }

      // 7.6 Identifier Names and Identifiers

      function fromCodePoint(cp) {
          if (cp <= 0xFFFF) { return String.fromCharCode(cp); }
          var cu1 = String.fromCharCode(Math.floor((cp - 0x10000) / 0x400) + 0xD800);
          var cu2 = String.fromCharCode(((cp - 0x10000) % 0x400) + 0xDC00);
          return cu1 + cu2;
      }

      IDENTIFIER_START = new Array(0x80);
      for(ch = 0; ch < 0x80; ++ch) {
          IDENTIFIER_START[ch] =
              ch >= 0x61 && ch <= 0x7A ||  // a..z
              ch >= 0x41 && ch <= 0x5A ||  // A..Z
              ch === 0x24 || ch === 0x5F;  // $ (dollar) and _ (underscore)
      }

      IDENTIFIER_PART = new Array(0x80);
      for(ch = 0; ch < 0x80; ++ch) {
          IDENTIFIER_PART[ch] =
              ch >= 0x61 && ch <= 0x7A ||  // a..z
              ch >= 0x41 && ch <= 0x5A ||  // A..Z
              ch >= 0x30 && ch <= 0x39 ||  // 0..9
              ch === 0x24 || ch === 0x5F;  // $ (dollar) and _ (underscore)
      }

      function isIdentifierStartES5(ch) {
          return ch < 0x80 ? IDENTIFIER_START[ch] : ES5Regex.NonAsciiIdentifierStart.test(fromCodePoint(ch));
      }

      function isIdentifierPartES5(ch) {
          return ch < 0x80 ? IDENTIFIER_PART[ch] : ES5Regex.NonAsciiIdentifierPart.test(fromCodePoint(ch));
      }

      function isIdentifierStartES6(ch) {
          return ch < 0x80 ? IDENTIFIER_START[ch] : ES6Regex.NonAsciiIdentifierStart.test(fromCodePoint(ch));
      }

      function isIdentifierPartES6(ch) {
          return ch < 0x80 ? IDENTIFIER_PART[ch] : ES6Regex.NonAsciiIdentifierPart.test(fromCodePoint(ch));
      }

      code.exports = {
          isDecimalDigit: isDecimalDigit,
          isHexDigit: isHexDigit,
          isOctalDigit: isOctalDigit,
          isWhiteSpace: isWhiteSpace,
          isLineTerminator: isLineTerminator,
          isIdentifierStartES5: isIdentifierStartES5,
          isIdentifierPartES5: isIdentifierPartES5,
          isIdentifierStartES6: isIdentifierStartES6,
          isIdentifierPartES6: isIdentifierPartES6
      };
  }());
  /* vim: set sw=4 ts=4 et tw=80 : */

  var codeExports = code.exports;

  var keyword = {exports: {}};

  /*
    Copyright (C) 2013 Yusuke Suzuki <utatane.tea@gmail.com>

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:

      * Redistributions of source code must retain the above copyright
        notice, this list of conditions and the following disclaimer.
      * Redistributions in binary form must reproduce the above copyright
        notice, this list of conditions and the following disclaimer in the
        documentation and/or other materials provided with the distribution.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
    DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
    ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
    THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */

  (function () {

      var code = codeExports;

      function isStrictModeReservedWordES6(id) {
          switch (id) {
          case 'implements':
          case 'interface':
          case 'package':
          case 'private':
          case 'protected':
          case 'public':
          case 'static':
          case 'let':
              return true;
          default:
              return false;
          }
      }

      function isKeywordES5(id, strict) {
          // yield should not be treated as keyword under non-strict mode.
          if (!strict && id === 'yield') {
              return false;
          }
          return isKeywordES6(id, strict);
      }

      function isKeywordES6(id, strict) {
          if (strict && isStrictModeReservedWordES6(id)) {
              return true;
          }

          switch (id.length) {
          case 2:
              return (id === 'if') || (id === 'in') || (id === 'do');
          case 3:
              return (id === 'var') || (id === 'for') || (id === 'new') || (id === 'try');
          case 4:
              return (id === 'this') || (id === 'else') || (id === 'case') ||
                  (id === 'void') || (id === 'with') || (id === 'enum');
          case 5:
              return (id === 'while') || (id === 'break') || (id === 'catch') ||
                  (id === 'throw') || (id === 'const') || (id === 'yield') ||
                  (id === 'class') || (id === 'super');
          case 6:
              return (id === 'return') || (id === 'typeof') || (id === 'delete') ||
                  (id === 'switch') || (id === 'export') || (id === 'import');
          case 7:
              return (id === 'default') || (id === 'finally') || (id === 'extends');
          case 8:
              return (id === 'function') || (id === 'continue') || (id === 'debugger');
          case 10:
              return (id === 'instanceof');
          default:
              return false;
          }
      }

      function isReservedWordES5(id, strict) {
          return id === 'null' || id === 'true' || id === 'false' || isKeywordES5(id, strict);
      }

      function isReservedWordES6(id, strict) {
          return id === 'null' || id === 'true' || id === 'false' || isKeywordES6(id, strict);
      }

      function isRestrictedWord(id) {
          return id === 'eval' || id === 'arguments';
      }

      function isIdentifierNameES5(id) {
          var i, iz, ch;

          if (id.length === 0) { return false; }

          ch = id.charCodeAt(0);
          if (!code.isIdentifierStartES5(ch)) {
              return false;
          }

          for (i = 1, iz = id.length; i < iz; ++i) {
              ch = id.charCodeAt(i);
              if (!code.isIdentifierPartES5(ch)) {
                  return false;
              }
          }
          return true;
      }

      function decodeUtf16(lead, trail) {
          return (lead - 0xD800) * 0x400 + (trail - 0xDC00) + 0x10000;
      }

      function isIdentifierNameES6(id) {
          var i, iz, ch, lowCh, check;

          if (id.length === 0) { return false; }

          check = code.isIdentifierStartES6;
          for (i = 0, iz = id.length; i < iz; ++i) {
              ch = id.charCodeAt(i);
              if (0xD800 <= ch && ch <= 0xDBFF) {
                  ++i;
                  if (i >= iz) { return false; }
                  lowCh = id.charCodeAt(i);
                  if (!(0xDC00 <= lowCh && lowCh <= 0xDFFF)) {
                      return false;
                  }
                  ch = decodeUtf16(ch, lowCh);
              }
              if (!check(ch)) {
                  return false;
              }
              check = code.isIdentifierPartES6;
          }
          return true;
      }

      function isIdentifierES5(id, strict) {
          return isIdentifierNameES5(id) && !isReservedWordES5(id, strict);
      }

      function isIdentifierES6(id, strict) {
          return isIdentifierNameES6(id) && !isReservedWordES6(id, strict);
      }

      keyword.exports = {
          isKeywordES5: isKeywordES5,
          isKeywordES6: isKeywordES6,
          isReservedWordES5: isReservedWordES5,
          isReservedWordES6: isReservedWordES6,
          isRestrictedWord: isRestrictedWord,
          isIdentifierNameES5: isIdentifierNameES5,
          isIdentifierNameES6: isIdentifierNameES6,
          isIdentifierES5: isIdentifierES5,
          isIdentifierES6: isIdentifierES6
      };
  }());
  /* vim: set sw=4 ts=4 et tw=80 : */

  var keywordExports = keyword.exports;

  /*
    Copyright (C) 2013 Yusuke Suzuki <utatane.tea@gmail.com>

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:

      * Redistributions of source code must retain the above copyright
        notice, this list of conditions and the following disclaimer.
      * Redistributions in binary form must reproduce the above copyright
        notice, this list of conditions and the following disclaimer in the
        documentation and/or other materials provided with the distribution.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
    DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
    ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
    THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
  (function () {

      utils.ast = astExports;
      utils.code = codeExports;
      utils.keyword = keywordExports;
  }());

  var sourceMap = {};

  var sourceMapGenerator = {};

  var base64Vlq = {};

  var base64 = {};

  /* -*- Mode: js; js-indent-level: 2; -*- */

  var hasRequiredBase64;

  function requireBase64 () {
  	if (hasRequiredBase64) return base64;
  	hasRequiredBase64 = 1;
  	/*
  	 * Copyright 2011 Mozilla Foundation and contributors
  	 * Licensed under the New BSD license. See LICENSE or:
  	 * http://opensource.org/licenses/BSD-3-Clause
  	 */

  	var intToCharMap = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('');

  	/**
  	 * Encode an integer in the range of 0 to 63 to a single base 64 digit.
  	 */
  	base64.encode = function (number) {
  	  if (0 <= number && number < intToCharMap.length) {
  	    return intToCharMap[number];
  	  }
  	  throw new TypeError("Must be between 0 and 63: " + number);
  	};

  	/**
  	 * Decode a single base 64 character code digit to an integer. Returns -1 on
  	 * failure.
  	 */
  	base64.decode = function (charCode) {
  	  var bigA = 65;     // 'A'
  	  var bigZ = 90;     // 'Z'

  	  var littleA = 97;  // 'a'
  	  var littleZ = 122; // 'z'

  	  var zero = 48;     // '0'
  	  var nine = 57;     // '9'

  	  var plus = 43;     // '+'
  	  var slash = 47;    // '/'

  	  var littleOffset = 26;
  	  var numberOffset = 52;

  	  // 0 - 25: ABCDEFGHIJKLMNOPQRSTUVWXYZ
  	  if (bigA <= charCode && charCode <= bigZ) {
  	    return (charCode - bigA);
  	  }

  	  // 26 - 51: abcdefghijklmnopqrstuvwxyz
  	  if (littleA <= charCode && charCode <= littleZ) {
  	    return (charCode - littleA + littleOffset);
  	  }

  	  // 52 - 61: 0123456789
  	  if (zero <= charCode && charCode <= nine) {
  	    return (charCode - zero + numberOffset);
  	  }

  	  // 62: +
  	  if (charCode == plus) {
  	    return 62;
  	  }

  	  // 63: /
  	  if (charCode == slash) {
  	    return 63;
  	  }

  	  // Invalid base64 digit.
  	  return -1;
  	};
  	return base64;
  }

  /* -*- Mode: js; js-indent-level: 2; -*- */

  var hasRequiredBase64Vlq;

  function requireBase64Vlq () {
  	if (hasRequiredBase64Vlq) return base64Vlq;
  	hasRequiredBase64Vlq = 1;
  	/*
  	 * Copyright 2011 Mozilla Foundation and contributors
  	 * Licensed under the New BSD license. See LICENSE or:
  	 * http://opensource.org/licenses/BSD-3-Clause
  	 *
  	 * Based on the Base 64 VLQ implementation in Closure Compiler:
  	 * https://code.google.com/p/closure-compiler/source/browse/trunk/src/com/google/debugging/sourcemap/Base64VLQ.java
  	 *
  	 * Copyright 2011 The Closure Compiler Authors. All rights reserved.
  	 * Redistribution and use in source and binary forms, with or without
  	 * modification, are permitted provided that the following conditions are
  	 * met:
  	 *
  	 *  * Redistributions of source code must retain the above copyright
  	 *    notice, this list of conditions and the following disclaimer.
  	 *  * Redistributions in binary form must reproduce the above
  	 *    copyright notice, this list of conditions and the following
  	 *    disclaimer in the documentation and/or other materials provided
  	 *    with the distribution.
  	 *  * Neither the name of Google Inc. nor the names of its
  	 *    contributors may be used to endorse or promote products derived
  	 *    from this software without specific prior written permission.
  	 *
  	 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  	 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  	 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  	 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  	 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  	 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  	 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  	 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  	 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  	 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  	 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  	 */

  	var base64 = requireBase64();

  	// A single base 64 digit can contain 6 bits of data. For the base 64 variable
  	// length quantities we use in the source map spec, the first bit is the sign,
  	// the next four bits are the actual value, and the 6th bit is the
  	// continuation bit. The continuation bit tells us whether there are more
  	// digits in this value following this digit.
  	//
  	//   Continuation
  	//   |    Sign
  	//   |    |
  	//   V    V
  	//   101011

  	var VLQ_BASE_SHIFT = 5;

  	// binary: 100000
  	var VLQ_BASE = 1 << VLQ_BASE_SHIFT;

  	// binary: 011111
  	var VLQ_BASE_MASK = VLQ_BASE - 1;

  	// binary: 100000
  	var VLQ_CONTINUATION_BIT = VLQ_BASE;

  	/**
  	 * Converts from a two-complement value to a value where the sign bit is
  	 * placed in the least significant bit.  For example, as decimals:
  	 *   1 becomes 2 (10 binary), -1 becomes 3 (11 binary)
  	 *   2 becomes 4 (100 binary), -2 becomes 5 (101 binary)
  	 */
  	function toVLQSigned(aValue) {
  	  return aValue < 0
  	    ? ((-aValue) << 1) + 1
  	    : (aValue << 1) + 0;
  	}

  	/**
  	 * Converts to a two-complement value from a value where the sign bit is
  	 * placed in the least significant bit.  For example, as decimals:
  	 *   2 (10 binary) becomes 1, 3 (11 binary) becomes -1
  	 *   4 (100 binary) becomes 2, 5 (101 binary) becomes -2
  	 */
  	function fromVLQSigned(aValue) {
  	  var isNegative = (aValue & 1) === 1;
  	  var shifted = aValue >> 1;
  	  return isNegative
  	    ? -shifted
  	    : shifted;
  	}

  	/**
  	 * Returns the base 64 VLQ encoded value.
  	 */
  	base64Vlq.encode = function base64VLQ_encode(aValue) {
  	  var encoded = "";
  	  var digit;

  	  var vlq = toVLQSigned(aValue);

  	  do {
  	    digit = vlq & VLQ_BASE_MASK;
  	    vlq >>>= VLQ_BASE_SHIFT;
  	    if (vlq > 0) {
  	      // There are still more digits in this value, so we must make sure the
  	      // continuation bit is marked.
  	      digit |= VLQ_CONTINUATION_BIT;
  	    }
  	    encoded += base64.encode(digit);
  	  } while (vlq > 0);

  	  return encoded;
  	};

  	/**
  	 * Decodes the next base 64 VLQ value from the given string and returns the
  	 * value and the rest of the string via the out parameter.
  	 */
  	base64Vlq.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) {
  	  var strLen = aStr.length;
  	  var result = 0;
  	  var shift = 0;
  	  var continuation, digit;

  	  do {
  	    if (aIndex >= strLen) {
  	      throw new Error("Expected more digits in base 64 VLQ value.");
  	    }

  	    digit = base64.decode(aStr.charCodeAt(aIndex++));
  	    if (digit === -1) {
  	      throw new Error("Invalid base64 digit: " + aStr.charAt(aIndex - 1));
  	    }

  	    continuation = !!(digit & VLQ_CONTINUATION_BIT);
  	    digit &= VLQ_BASE_MASK;
  	    result = result + (digit << shift);
  	    shift += VLQ_BASE_SHIFT;
  	  } while (continuation);

  	  aOutParam.value = fromVLQSigned(result);
  	  aOutParam.rest = aIndex;
  	};
  	return base64Vlq;
  }

  var util = {};

  /* -*- Mode: js; js-indent-level: 2; -*- */

  var hasRequiredUtil;

  function requireUtil () {
  	if (hasRequiredUtil) return util;
  	hasRequiredUtil = 1;
  	(function (exports$1) {
  		/*
  		 * Copyright 2011 Mozilla Foundation and contributors
  		 * Licensed under the New BSD license. See LICENSE or:
  		 * http://opensource.org/licenses/BSD-3-Clause
  		 */

  		/**
  		 * This is a helper function for getting values from parameter/options
  		 * objects.
  		 *
  		 * @param args The object we are extracting values from
  		 * @param name The name of the property we are getting.
  		 * @param defaultValue An optional value to return if the property is missing
  		 * from the object. If this is not specified and the property is missing, an
  		 * error will be thrown.
  		 */
  		function getArg(aArgs, aName, aDefaultValue) {
  		  if (aName in aArgs) {
  		    return aArgs[aName];
  		  } else if (arguments.length === 3) {
  		    return aDefaultValue;
  		  } else {
  		    throw new Error('"' + aName + '" is a required argument.');
  		  }
  		}
  		exports$1.getArg = getArg;

  		var urlRegexp = /^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.-]*)(?::(\d+))?(.*)$/;
  		var dataUrlRegexp = /^data:.+\,.+$/;

  		function urlParse(aUrl) {
  		  var match = aUrl.match(urlRegexp);
  		  if (!match) {
  		    return null;
  		  }
  		  return {
  		    scheme: match[1],
  		    auth: match[2],
  		    host: match[3],
  		    port: match[4],
  		    path: match[5]
  		  };
  		}
  		exports$1.urlParse = urlParse;

  		function urlGenerate(aParsedUrl) {
  		  var url = '';
  		  if (aParsedUrl.scheme) {
  		    url += aParsedUrl.scheme + ':';
  		  }
  		  url += '//';
  		  if (aParsedUrl.auth) {
  		    url += aParsedUrl.auth + '@';
  		  }
  		  if (aParsedUrl.host) {
  		    url += aParsedUrl.host;
  		  }
  		  if (aParsedUrl.port) {
  		    url += ":" + aParsedUrl.port;
  		  }
  		  if (aParsedUrl.path) {
  		    url += aParsedUrl.path;
  		  }
  		  return url;
  		}
  		exports$1.urlGenerate = urlGenerate;

  		/**
  		 * Normalizes a path, or the path portion of a URL:
  		 *
  		 * - Replaces consecutive slashes with one slash.
  		 * - Removes unnecessary '.' parts.
  		 * - Removes unnecessary '<dir>/..' parts.
  		 *
  		 * Based on code in the Node.js 'path' core module.
  		 *
  		 * @param aPath The path or url to normalize.
  		 */
  		function normalize(aPath) {
  		  var path = aPath;
  		  var url = urlParse(aPath);
  		  if (url) {
  		    if (!url.path) {
  		      return aPath;
  		    }
  		    path = url.path;
  		  }
  		  var isAbsolute = exports$1.isAbsolute(path);

  		  var parts = path.split(/\/+/);
  		  for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
  		    part = parts[i];
  		    if (part === '.') {
  		      parts.splice(i, 1);
  		    } else if (part === '..') {
  		      up++;
  		    } else if (up > 0) {
  		      if (part === '') {
  		        // The first part is blank if the path is absolute. Trying to go
  		        // above the root is a no-op. Therefore we can remove all '..' parts
  		        // directly after the root.
  		        parts.splice(i + 1, up);
  		        up = 0;
  		      } else {
  		        parts.splice(i, 2);
  		        up--;
  		      }
  		    }
  		  }
  		  path = parts.join('/');

  		  if (path === '') {
  		    path = isAbsolute ? '/' : '.';
  		  }

  		  if (url) {
  		    url.path = path;
  		    return urlGenerate(url);
  		  }
  		  return path;
  		}
  		exports$1.normalize = normalize;

  		/**
  		 * Joins two paths/URLs.
  		 *
  		 * @param aRoot The root path or URL.
  		 * @param aPath The path or URL to be joined with the root.
  		 *
  		 * - If aPath is a URL or a data URI, aPath is returned, unless aPath is a
  		 *   scheme-relative URL: Then the scheme of aRoot, if any, is prepended
  		 *   first.
  		 * - Otherwise aPath is a path. If aRoot is a URL, then its path portion
  		 *   is updated with the result and aRoot is returned. Otherwise the result
  		 *   is returned.
  		 *   - If aPath is absolute, the result is aPath.
  		 *   - Otherwise the two paths are joined with a slash.
  		 * - Joining for example 'http://' and 'www.example.com' is also supported.
  		 */
  		function join(aRoot, aPath) {
  		  if (aRoot === "") {
  		    aRoot = ".";
  		  }
  		  if (aPath === "") {
  		    aPath = ".";
  		  }
  		  var aPathUrl = urlParse(aPath);
  		  var aRootUrl = urlParse(aRoot);
  		  if (aRootUrl) {
  		    aRoot = aRootUrl.path || '/';
  		  }

  		  // `join(foo, '//www.example.org')`
  		  if (aPathUrl && !aPathUrl.scheme) {
  		    if (aRootUrl) {
  		      aPathUrl.scheme = aRootUrl.scheme;
  		    }
  		    return urlGenerate(aPathUrl);
  		  }

  		  if (aPathUrl || aPath.match(dataUrlRegexp)) {
  		    return aPath;
  		  }

  		  // `join('http://', 'www.example.com')`
  		  if (aRootUrl && !aRootUrl.host && !aRootUrl.path) {
  		    aRootUrl.host = aPath;
  		    return urlGenerate(aRootUrl);
  		  }

  		  var joined = aPath.charAt(0) === '/'
  		    ? aPath
  		    : normalize(aRoot.replace(/\/+$/, '') + '/' + aPath);

  		  if (aRootUrl) {
  		    aRootUrl.path = joined;
  		    return urlGenerate(aRootUrl);
  		  }
  		  return joined;
  		}
  		exports$1.join = join;

  		exports$1.isAbsolute = function (aPath) {
  		  return aPath.charAt(0) === '/' || urlRegexp.test(aPath);
  		};

  		/**
  		 * Make a path relative to a URL or another path.
  		 *
  		 * @param aRoot The root path or URL.
  		 * @param aPath The path or URL to be made relative to aRoot.
  		 */
  		function relative(aRoot, aPath) {
  		  if (aRoot === "") {
  		    aRoot = ".";
  		  }

  		  aRoot = aRoot.replace(/\/$/, '');

  		  // It is possible for the path to be above the root. In this case, simply
  		  // checking whether the root is a prefix of the path won't work. Instead, we
  		  // need to remove components from the root one by one, until either we find
  		  // a prefix that fits, or we run out of components to remove.
  		  var level = 0;
  		  while (aPath.indexOf(aRoot + '/') !== 0) {
  		    var index = aRoot.lastIndexOf("/");
  		    if (index < 0) {
  		      return aPath;
  		    }

  		    // If the only part of the root that is left is the scheme (i.e. http://,
  		    // file:///, etc.), one or more slashes (/), or simply nothing at all, we
  		    // have exhausted all components, so the path is not relative to the root.
  		    aRoot = aRoot.slice(0, index);
  		    if (aRoot.match(/^([^\/]+:\/)?\/*$/)) {
  		      return aPath;
  		    }

  		    ++level;
  		  }

  		  // Make sure we add a "../" for each component we removed from the root.
  		  return Array(level + 1).join("../") + aPath.substr(aRoot.length + 1);
  		}
  		exports$1.relative = relative;

  		var supportsNullProto = (function () {
  		  var obj = Object.create(null);
  		  return !('__proto__' in obj);
  		}());

  		function identity (s) {
  		  return s;
  		}

  		/**
  		 * Because behavior goes wacky when you set `__proto__` on objects, we
  		 * have to prefix all the strings in our set with an arbitrary character.
  		 *
  		 * See https://github.com/mozilla/source-map/pull/31 and
  		 * https://github.com/mozilla/source-map/issues/30
  		 *
  		 * @param String aStr
  		 */
  		function toSetString(aStr) {
  		  if (isProtoString(aStr)) {
  		    return '$' + aStr;
  		  }

  		  return aStr;
  		}
  		exports$1.toSetString = supportsNullProto ? identity : toSetString;

  		function fromSetString(aStr) {
  		  if (isProtoString(aStr)) {
  		    return aStr.slice(1);
  		  }

  		  return aStr;
  		}
  		exports$1.fromSetString = supportsNullProto ? identity : fromSetString;

  		function isProtoString(s) {
  		  if (!s) {
  		    return false;
  		  }

  		  var length = s.length;

  		  if (length < 9 /* "__proto__".length */) {
  		    return false;
  		  }

  		  if (s.charCodeAt(length - 1) !== 95  /* '_' */ ||
  		      s.charCodeAt(length - 2) !== 95  /* '_' */ ||
  		      s.charCodeAt(length - 3) !== 111 /* 'o' */ ||
  		      s.charCodeAt(length - 4) !== 116 /* 't' */ ||
  		      s.charCodeAt(length - 5) !== 111 /* 'o' */ ||
  		      s.charCodeAt(length - 6) !== 114 /* 'r' */ ||
  		      s.charCodeAt(length - 7) !== 112 /* 'p' */ ||
  		      s.charCodeAt(length - 8) !== 95  /* '_' */ ||
  		      s.charCodeAt(length - 9) !== 95  /* '_' */) {
  		    return false;
  		  }

  		  for (var i = length - 10; i >= 0; i--) {
  		    if (s.charCodeAt(i) !== 36 /* '$' */) {
  		      return false;
  		    }
  		  }

  		  return true;
  		}

  		/**
  		 * Comparator between two mappings where the original positions are compared.
  		 *
  		 * Optionally pass in `true` as `onlyCompareGenerated` to consider two
  		 * mappings with the same original source/line/column, but different generated
  		 * line and column the same. Useful when searching for a mapping with a
  		 * stubbed out mapping.
  		 */
  		function compareByOriginalPositions(mappingA, mappingB, onlyCompareOriginal) {
  		  var cmp = strcmp(mappingA.source, mappingB.source);
  		  if (cmp !== 0) {
  		    return cmp;
  		  }

  		  cmp = mappingA.originalLine - mappingB.originalLine;
  		  if (cmp !== 0) {
  		    return cmp;
  		  }

  		  cmp = mappingA.originalColumn - mappingB.originalColumn;
  		  if (cmp !== 0 || onlyCompareOriginal) {
  		    return cmp;
  		  }

  		  cmp = mappingA.generatedColumn - mappingB.generatedColumn;
  		  if (cmp !== 0) {
  		    return cmp;
  		  }

  		  cmp = mappingA.generatedLine - mappingB.generatedLine;
  		  if (cmp !== 0) {
  		    return cmp;
  		  }

  		  return strcmp(mappingA.name, mappingB.name);
  		}
  		exports$1.compareByOriginalPositions = compareByOriginalPositions;

  		/**
  		 * Comparator between two mappings with deflated source and name indices where
  		 * the generated positions are compared.
  		 *
  		 * Optionally pass in `true` as `onlyCompareGenerated` to consider two
  		 * mappings with the same generated line and column, but different
  		 * source/name/original line and column the same. Useful when searching for a
  		 * mapping with a stubbed out mapping.
  		 */
  		function compareByGeneratedPositionsDeflated(mappingA, mappingB, onlyCompareGenerated) {
  		  var cmp = mappingA.generatedLine - mappingB.generatedLine;
  		  if (cmp !== 0) {
  		    return cmp;
  		  }

  		  cmp = mappingA.generatedColumn - mappingB.generatedColumn;
  		  if (cmp !== 0 || onlyCompareGenerated) {
  		    return cmp;
  		  }

  		  cmp = strcmp(mappingA.source, mappingB.source);
  		  if (cmp !== 0) {
  		    return cmp;
  		  }

  		  cmp = mappingA.originalLine - mappingB.originalLine;
  		  if (cmp !== 0) {
  		    return cmp;
  		  }

  		  cmp = mappingA.originalColumn - mappingB.originalColumn;
  		  if (cmp !== 0) {
  		    return cmp;
  		  }

  		  return strcmp(mappingA.name, mappingB.name);
  		}
  		exports$1.compareByGeneratedPositionsDeflated = compareByGeneratedPositionsDeflated;

  		function strcmp(aStr1, aStr2) {
  		  if (aStr1 === aStr2) {
  		    return 0;
  		  }

  		  if (aStr1 === null) {
  		    return 1; // aStr2 !== null
  		  }

  		  if (aStr2 === null) {
  		    return -1; // aStr1 !== null
  		  }

  		  if (aStr1 > aStr2) {
  		    return 1;
  		  }

  		  return -1;
  		}

  		/**
  		 * Comparator between two mappings with inflated source and name strings where
  		 * the generated positions are compared.
  		 */
  		function compareByGeneratedPositionsInflated(mappingA, mappingB) {
  		  var cmp = mappingA.generatedLine - mappingB.generatedLine;
  		  if (cmp !== 0) {
  		    return cmp;
  		  }

  		  cmp = mappingA.generatedColumn - mappingB.generatedColumn;
  		  if (cmp !== 0) {
  		    return cmp;
  		  }

  		  cmp = strcmp(mappingA.source, mappingB.source);
  		  if (cmp !== 0) {
  		    return cmp;
  		  }

  		  cmp = mappingA.originalLine - mappingB.originalLine;
  		  if (cmp !== 0) {
  		    return cmp;
  		  }

  		  cmp = mappingA.originalColumn - mappingB.originalColumn;
  		  if (cmp !== 0) {
  		    return cmp;
  		  }

  		  return strcmp(mappingA.name, mappingB.name);
  		}
  		exports$1.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflated;

  		/**
  		 * Strip any JSON XSSI avoidance prefix from the string (as documented
  		 * in the source maps specification), and then parse the string as
  		 * JSON.
  		 */
  		function parseSourceMapInput(str) {
  		  return JSON.parse(str.replace(/^\)]}'[^\n]*\n/, ''));
  		}
  		exports$1.parseSourceMapInput = parseSourceMapInput;

  		/**
  		 * Compute the URL of a source given the the source root, the source's
  		 * URL, and the source map's URL.
  		 */
  		function computeSourceURL(sourceRoot, sourceURL, sourceMapURL) {
  		  sourceURL = sourceURL || '';

  		  if (sourceRoot) {
  		    // This follows what Chrome does.
  		    if (sourceRoot[sourceRoot.length - 1] !== '/' && sourceURL[0] !== '/') {
  		      sourceRoot += '/';
  		    }
  		    // The spec says:
  		    //   Line 4: An optional source root, useful for relocating source
  		    //   files on a server or removing repeated values in the
  		    //   “sources” entry.  This value is prepended to the individual
  		    //   entries in the “source” field.
  		    sourceURL = sourceRoot + sourceURL;
  		  }

  		  // Historically, SourceMapConsumer did not take the sourceMapURL as
  		  // a parameter.  This mode is still somewhat supported, which is why
  		  // this code block is conditional.  However, it's preferable to pass
  		  // the source map URL to SourceMapConsumer, so that this function
  		  // can implement the source URL resolution algorithm as outlined in
  		  // the spec.  This block is basically the equivalent of:
  		  //    new URL(sourceURL, sourceMapURL).toString()
  		  // ... except it avoids using URL, which wasn't available in the
  		  // older releases of node still supported by this library.
  		  //
  		  // The spec says:
  		  //   If the sources are not absolute URLs after prepending of the
  		  //   “sourceRoot”, the sources are resolved relative to the
  		  //   SourceMap (like resolving script src in a html document).
  		  if (sourceMapURL) {
  		    var parsed = urlParse(sourceMapURL);
  		    if (!parsed) {
  		      throw new Error("sourceMapURL could not be parsed");
  		    }
  		    if (parsed.path) {
  		      // Strip the last path component, but keep the "/".
  		      var index = parsed.path.lastIndexOf('/');
  		      if (index >= 0) {
  		        parsed.path = parsed.path.substring(0, index + 1);
  		      }
  		    }
  		    sourceURL = join(urlGenerate(parsed), sourceURL);
  		  }

  		  return normalize(sourceURL);
  		}
  		exports$1.computeSourceURL = computeSourceURL; 
  	} (util));
  	return util;
  }

  var arraySet = {};

  /* -*- Mode: js; js-indent-level: 2; -*- */

  var hasRequiredArraySet;

  function requireArraySet () {
  	if (hasRequiredArraySet) return arraySet;
  	hasRequiredArraySet = 1;
  	/*
  	 * Copyright 2011 Mozilla Foundation and contributors
  	 * Licensed under the New BSD license. See LICENSE or:
  	 * http://opensource.org/licenses/BSD-3-Clause
  	 */

  	var util = requireUtil();
  	var has = Object.prototype.hasOwnProperty;
  	var hasNativeMap = typeof Map !== "undefined";

  	/**
  	 * A data structure which is a combination of an array and a set. Adding a new
  	 * member is O(1), testing for membership is O(1), and finding the index of an
  	 * element is O(1). Removing elements from the set is not supported. Only
  	 * strings are supported for membership.
  	 */
  	function ArraySet() {
  	  this._array = [];
  	  this._set = hasNativeMap ? new Map() : Object.create(null);
  	}

  	/**
  	 * Static method for creating ArraySet instances from an existing array.
  	 */
  	ArraySet.fromArray = function ArraySet_fromArray(aArray, aAllowDuplicates) {
  	  var set = new ArraySet();
  	  for (var i = 0, len = aArray.length; i < len; i++) {
  	    set.add(aArray[i], aAllowDuplicates);
  	  }
  	  return set;
  	};

  	/**
  	 * Return how many unique items are in this ArraySet. If duplicates have been
  	 * added, than those do not count towards the size.
  	 *
  	 * @returns Number
  	 */
  	ArraySet.prototype.size = function ArraySet_size() {
  	  return hasNativeMap ? this._set.size : Object.getOwnPropertyNames(this._set).length;
  	};

  	/**
  	 * Add the given string to this set.
  	 *
  	 * @param String aStr
  	 */
  	ArraySet.prototype.add = function ArraySet_add(aStr, aAllowDuplicates) {
  	  var sStr = hasNativeMap ? aStr : util.toSetString(aStr);
  	  var isDuplicate = hasNativeMap ? this.has(aStr) : has.call(this._set, sStr);
  	  var idx = this._array.length;
  	  if (!isDuplicate || aAllowDuplicates) {
  	    this._array.push(aStr);
  	  }
  	  if (!isDuplicate) {
  	    if (hasNativeMap) {
  	      this._set.set(aStr, idx);
  	    } else {
  	      this._set[sStr] = idx;
  	    }
  	  }
  	};

  	/**
  	 * Is the given string a member of this set?
  	 *
  	 * @param String aStr
  	 */
  	ArraySet.prototype.has = function ArraySet_has(aStr) {
  	  if (hasNativeMap) {
  	    return this._set.has(aStr);
  	  } else {
  	    var sStr = util.toSetString(aStr);
  	    return has.call(this._set, sStr);
  	  }
  	};

  	/**
  	 * What is the index of the given string in the array?
  	 *
  	 * @param String aStr
  	 */
  	ArraySet.prototype.indexOf = function ArraySet_indexOf(aStr) {
  	  if (hasNativeMap) {
  	    var idx = this._set.get(aStr);
  	    if (idx >= 0) {
  	        return idx;
  	    }
  	  } else {
  	    var sStr = util.toSetString(aStr);
  	    if (has.call(this._set, sStr)) {
  	      return this._set[sStr];
  	    }
  	  }

  	  throw new Error('"' + aStr + '" is not in the set.');
  	};

  	/**
  	 * What is the element at the given index?
  	 *
  	 * @param Number aIdx
  	 */
  	ArraySet.prototype.at = function ArraySet_at(aIdx) {
  	  if (aIdx >= 0 && aIdx < this._array.length) {
  	    return this._array[aIdx];
  	  }
  	  throw new Error('No element indexed by ' + aIdx);
  	};

  	/**
  	 * Returns the array representation of this set (which has the proper indices
  	 * indicated by indexOf). Note that this is a copy of the internal array used
  	 * for storing the members so that no one can mess with internal state.
  	 */
  	ArraySet.prototype.toArray = function ArraySet_toArray() {
  	  return this._array.slice();
  	};

  	arraySet.ArraySet = ArraySet;
  	return arraySet;
  }

  var mappingList = {};

  /* -*- Mode: js; js-indent-level: 2; -*- */

  var hasRequiredMappingList;

  function requireMappingList () {
  	if (hasRequiredMappingList) return mappingList;
  	hasRequiredMappingList = 1;
  	/*
  	 * Copyright 2014 Mozilla Foundation and contributors
  	 * Licensed under the New BSD license. See LICENSE or:
  	 * http://opensource.org/licenses/BSD-3-Clause
  	 */

  	var util = requireUtil();

  	/**
  	 * Determine whether mappingB is after mappingA with respect to generated
  	 * position.
  	 */
  	function generatedPositionAfter(mappingA, mappingB) {
  	  // Optimized for most common case
  	  var lineA = mappingA.generatedLine;
  	  var lineB = mappingB.generatedLine;
  	  var columnA = mappingA.generatedColumn;
  	  var columnB = mappingB.generatedColumn;
  	  return lineB > lineA || lineB == lineA && columnB >= columnA ||
  	         util.compareByGeneratedPositionsInflated(mappingA, mappingB) <= 0;
  	}

  	/**
  	 * A data structure to provide a sorted view of accumulated mappings in a
  	 * performance conscious manner. It trades a neglibable overhead in general
  	 * case for a large speedup in case of mappings being added in order.
  	 */
  	function MappingList() {
  	  this._array = [];
  	  this._sorted = true;
  	  // Serves as infimum
  	  this._last = {generatedLine: -1, generatedColumn: 0};
  	}

  	/**
  	 * Iterate through internal items. This method takes the same arguments that
  	 * `Array.prototype.forEach` takes.
  	 *
  	 * NOTE: The order of the mappings is NOT guaranteed.
  	 */
  	MappingList.prototype.unsortedForEach =
  	  function MappingList_forEach(aCallback, aThisArg) {
  	    this._array.forEach(aCallback, aThisArg);
  	  };

  	/**
  	 * Add the given source mapping.
  	 *
  	 * @param Object aMapping
  	 */
  	MappingList.prototype.add = function MappingList_add(aMapping) {
  	  if (generatedPositionAfter(this._last, aMapping)) {
  	    this._last = aMapping;
  	    this._array.push(aMapping);
  	  } else {
  	    this._sorted = false;
  	    this._array.push(aMapping);
  	  }
  	};

  	/**
  	 * Returns the flat, sorted array of mappings. The mappings are sorted by
  	 * generated position.
  	 *
  	 * WARNING: This method returns internal data without copying, for
  	 * performance. The return value must NOT be mutated, and should be treated as
  	 * an immutable borrow. If you want to take ownership, you must make your own
  	 * copy.
  	 */
  	MappingList.prototype.toArray = function MappingList_toArray() {
  	  if (!this._sorted) {
  	    this._array.sort(util.compareByGeneratedPositionsInflated);
  	    this._sorted = true;
  	  }
  	  return this._array;
  	};

  	mappingList.MappingList = MappingList;
  	return mappingList;
  }

  /* -*- Mode: js; js-indent-level: 2; -*- */

  var hasRequiredSourceMapGenerator;

  function requireSourceMapGenerator () {
  	if (hasRequiredSourceMapGenerator) return sourceMapGenerator;
  	hasRequiredSourceMapGenerator = 1;
  	/*
  	 * Copyright 2011 Mozilla Foundation and contributors
  	 * Licensed under the New BSD license. See LICENSE or:
  	 * http://opensource.org/licenses/BSD-3-Clause
  	 */

  	var base64VLQ = requireBase64Vlq();
  	var util = requireUtil();
  	var ArraySet = requireArraySet().ArraySet;
  	var MappingList = requireMappingList().MappingList;

  	/**
  	 * An instance of the SourceMapGenerator represents a source map which is
  	 * being built incrementally. You may pass an object with the following
  	 * properties:
  	 *
  	 *   - file: The filename of the generated source.
  	 *   - sourceRoot: A root for all relative URLs in this source map.
  	 */
  	function SourceMapGenerator(aArgs) {
  	  if (!aArgs) {
  	    aArgs = {};
  	  }
  	  this._file = util.getArg(aArgs, 'file', null);
  	  this._sourceRoot = util.getArg(aArgs, 'sourceRoot', null);
  	  this._skipValidation = util.getArg(aArgs, 'skipValidation', false);
  	  this._sources = new ArraySet();
  	  this._names = new ArraySet();
  	  this._mappings = new MappingList();
  	  this._sourcesContents = null;
  	}

  	SourceMapGenerator.prototype._version = 3;

  	/**
  	 * Creates a new SourceMapGenerator based on a SourceMapConsumer
  	 *
  	 * @param aSourceMapConsumer The SourceMap.
  	 */
  	SourceMapGenerator.fromSourceMap =
  	  function SourceMapGenerator_fromSourceMap(aSourceMapConsumer) {
  	    var sourceRoot = aSourceMapConsumer.sourceRoot;
  	    var generator = new SourceMapGenerator({
  	      file: aSourceMapConsumer.file,
  	      sourceRoot: sourceRoot
  	    });
  	    aSourceMapConsumer.eachMapping(function (mapping) {
  	      var newMapping = {
  	        generated: {
  	          line: mapping.generatedLine,
  	          column: mapping.generatedColumn
  	        }
  	      };

  	      if (mapping.source != null) {
  	        newMapping.source = mapping.source;
  	        if (sourceRoot != null) {
  	          newMapping.source = util.relative(sourceRoot, newMapping.source);
  	        }

  	        newMapping.original = {
  	          line: mapping.originalLine,
  	          column: mapping.originalColumn
  	        };

  	        if (mapping.name != null) {
  	          newMapping.name = mapping.name;
  	        }
  	      }

  	      generator.addMapping(newMapping);
  	    });
  	    aSourceMapConsumer.sources.forEach(function (sourceFile) {
  	      var sourceRelative = sourceFile;
  	      if (sourceRoot !== null) {
  	        sourceRelative = util.relative(sourceRoot, sourceFile);
  	      }

  	      if (!generator._sources.has(sourceRelative)) {
  	        generator._sources.add(sourceRelative);
  	      }

  	      var content = aSourceMapConsumer.sourceContentFor(sourceFile);
  	      if (content != null) {
  	        generator.setSourceContent(sourceFile, content);
  	      }
  	    });
  	    return generator;
  	  };

  	/**
  	 * Add a single mapping from original source line and column to the generated
  	 * source's line and column for this source map being created. The mapping
  	 * object should have the following properties:
  	 *
  	 *   - generated: An object with the generated line and column positions.
  	 *   - original: An object with the original line and column positions.
  	 *   - source: The original source file (relative to the sourceRoot).
  	 *   - name: An optional original token name for this mapping.
  	 */
  	SourceMapGenerator.prototype.addMapping =
  	  function SourceMapGenerator_addMapping(aArgs) {
  	    var generated = util.getArg(aArgs, 'generated');
  	    var original = util.getArg(aArgs, 'original', null);
  	    var source = util.getArg(aArgs, 'source', null);
  	    var name = util.getArg(aArgs, 'name', null);

  	    if (!this._skipValidation) {
  	      this._validateMapping(generated, original, source, name);
  	    }

  	    if (source != null) {
  	      source = String(source);
  	      if (!this._sources.has(source)) {
  	        this._sources.add(source);
  	      }
  	    }

  	    if (name != null) {
  	      name = String(name);
  	      if (!this._names.has(name)) {
  	        this._names.add(name);
  	      }
  	    }

  	    this._mappings.add({
  	      generatedLine: generated.line,
  	      generatedColumn: generated.column,
  	      originalLine: original != null && original.line,
  	      originalColumn: original != null && original.column,
  	      source: source,
  	      name: name
  	    });
  	  };

  	/**
  	 * Set the source content for a source file.
  	 */
  	SourceMapGenerator.prototype.setSourceContent =
  	  function SourceMapGenerator_setSourceContent(aSourceFile, aSourceContent) {
  	    var source = aSourceFile;
  	    if (this._sourceRoot != null) {
  	      source = util.relative(this._sourceRoot, source);
  	    }

  	    if (aSourceContent != null) {
  	      // Add the source content to the _sourcesContents map.
  	      // Create a new _sourcesContents map if the property is null.
  	      if (!this._sourcesContents) {
  	        this._sourcesContents = Object.create(null);
  	      }
  	      this._sourcesContents[util.toSetString(source)] = aSourceContent;
  	    } else if (this._sourcesContents) {
  	      // Remove the source file from the _sourcesContents map.
  	      // If the _sourcesContents map is empty, set the property to null.
  	      delete this._sourcesContents[util.toSetString(source)];
  	      if (Object.keys(this._sourcesContents).length === 0) {
  	        this._sourcesContents = null;
  	      }
  	    }
  	  };

  	/**
  	 * Applies the mappings of a sub-source-map for a specific source file to the
  	 * source map being generated. Each mapping to the supplied source file is
  	 * rewritten using the supplied source map. Note: The resolution for the
  	 * resulting mappings is the minimium of this map and the supplied map.
  	 *
  	 * @param aSourceMapConsumer The source map to be applied.
  	 * @param aSourceFile Optional. The filename of the source file.
  	 *        If omitted, SourceMapConsumer's file property will be used.
  	 * @param aSourceMapPath Optional. The dirname of the path to the source map
  	 *        to be applied. If relative, it is relative to the SourceMapConsumer.
  	 *        This parameter is needed when the two source maps aren't in the same
  	 *        directory, and the source map to be applied contains relative source
  	 *        paths. If so, those relative source paths need to be rewritten
  	 *        relative to the SourceMapGenerator.
  	 */
  	SourceMapGenerator.prototype.applySourceMap =
  	  function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) {
  	    var sourceFile = aSourceFile;
  	    // If aSourceFile is omitted, we will use the file property of the SourceMap
  	    if (aSourceFile == null) {
  	      if (aSourceMapConsumer.file == null) {
  	        throw new Error(
  	          'SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, ' +
  	          'or the source map\'s "file" property. Both were omitted.'
  	        );
  	      }
  	      sourceFile = aSourceMapConsumer.file;
  	    }
  	    var sourceRoot = this._sourceRoot;
  	    // Make "sourceFile" relative if an absolute Url is passed.
  	    if (sourceRoot != null) {
  	      sourceFile = util.relative(sourceRoot, sourceFile);
  	    }
  	    // Applying the SourceMap can add and remove items from the sources and
  	    // the names array.
  	    var newSources = new ArraySet();
  	    var newNames = new ArraySet();

  	    // Find mappings for the "sourceFile"
  	    this._mappings.unsortedForEach(function (mapping) {
  	      if (mapping.source === sourceFile && mapping.originalLine != null) {
  	        // Check if it can be mapped by the source map, then update the mapping.
  	        var original = aSourceMapConsumer.originalPositionFor({
  	          line: mapping.originalLine,
  	          column: mapping.originalColumn
  	        });
  	        if (original.source != null) {
  	          // Copy mapping
  	          mapping.source = original.source;
  	          if (aSourceMapPath != null) {
  	            mapping.source = util.join(aSourceMapPath, mapping.source);
  	          }
  	          if (sourceRoot != null) {
  	            mapping.source = util.relative(sourceRoot, mapping.source);
  	          }
  	          mapping.originalLine = original.line;
  	          mapping.originalColumn = original.column;
  	          if (original.name != null) {
  	            mapping.name = original.name;
  	          }
  	        }
  	      }

  	      var source = mapping.source;
  	      if (source != null && !newSources.has(source)) {
  	        newSources.add(source);
  	      }

  	      var name = mapping.name;
  	      if (name != null && !newNames.has(name)) {
  	        newNames.add(name);
  	      }

  	    }, this);
  	    this._sources = newSources;
  	    this._names = newNames;

  	    // Copy sourcesContents of applied map.
  	    aSourceMapConsumer.sources.forEach(function (sourceFile) {
  	      var content = aSourceMapConsumer.sourceContentFor(sourceFile);
  	      if (content != null) {
  	        if (aSourceMapPath != null) {
  	          sourceFile = util.join(aSourceMapPath, sourceFile);
  	        }
  	        if (sourceRoot != null) {
  	          sourceFile = util.relative(sourceRoot, sourceFile);
  	        }
  	        this.setSourceContent(sourceFile, content);
  	      }
  	    }, this);
  	  };

  	/**
  	 * A mapping can have one of the three levels of data:
  	 *
  	 *   1. Just the generated position.
  	 *   2. The Generated position, original position, and original source.
  	 *   3. Generated and original position, original source, as well as a name
  	 *      token.
  	 *
  	 * To maintain consistency, we validate that any new mapping being added falls
  	 * in to one of these categories.
  	 */
  	SourceMapGenerator.prototype._validateMapping =
  	  function SourceMapGenerator_validateMapping(aGenerated, aOriginal, aSource,
  	                                              aName) {
  	    // When aOriginal is truthy but has empty values for .line and .column,
  	    // it is most likely a programmer error. In this case we throw a very
  	    // specific error message to try to guide them the right way.
  	    // For example: https://github.com/Polymer/polymer-bundler/pull/519
  	    if (aOriginal && typeof aOriginal.line !== 'number' && typeof aOriginal.column !== 'number') {
  	        throw new Error(
  	            'original.line and original.column are not numbers -- you probably meant to omit ' +
  	            'the original mapping entirely and only map the generated position. If so, pass ' +
  	            'null for the original mapping instead of an object with empty or null values.'
  	        );
  	    }

  	    if (aGenerated && 'line' in aGenerated && 'column' in aGenerated
  	        && aGenerated.line > 0 && aGenerated.column >= 0
  	        && !aOriginal && !aSource && !aName) {
  	      // Case 1.
  	      return;
  	    }
  	    else if (aGenerated && 'line' in aGenerated && 'column' in aGenerated
  	             && aOriginal && 'line' in aOriginal && 'column' in aOriginal
  	             && aGenerated.line > 0 && aGenerated.column >= 0
  	             && aOriginal.line > 0 && aOriginal.column >= 0
  	             && aSource) {
  	      // Cases 2 and 3.
  	      return;
  	    }
  	    else {
  	      throw new Error('Invalid mapping: ' + JSON.stringify({
  	        generated: aGenerated,
  	        source: aSource,
  	        original: aOriginal,
  	        name: aName
  	      }));
  	    }
  	  };

  	/**
  	 * Serialize the accumulated mappings in to the stream of base 64 VLQs
  	 * specified by the source map format.
  	 */
  	SourceMapGenerator.prototype._serializeMappings =
  	  function SourceMapGenerator_serializeMappings() {
  	    var previousGeneratedColumn = 0;
  	    var previousGeneratedLine = 1;
  	    var previousOriginalColumn = 0;
  	    var previousOriginalLine = 0;
  	    var previousName = 0;
  	    var previousSource = 0;
  	    var result = '';
  	    var next;
  	    var mapping;
  	    var nameIdx;
  	    var sourceIdx;

  	    var mappings = this._mappings.toArray();
  	    for (var i = 0, len = mappings.length; i < len; i++) {
  	      mapping = mappings[i];
  	      next = '';

  	      if (mapping.generatedLine !== previousGeneratedLine) {
  	        previousGeneratedColumn = 0;
  	        while (mapping.generatedLine !== previousGeneratedLine) {
  	          next += ';';
  	          previousGeneratedLine++;
  	        }
  	      }
  	      else {
  	        if (i > 0) {
  	          if (!util.compareByGeneratedPositionsInflated(mapping, mappings[i - 1])) {
  	            continue;
  	          }
  	          next += ',';
  	        }
  	      }

  	      next += base64VLQ.encode(mapping.generatedColumn
  	                                 - previousGeneratedColumn);
  	      previousGeneratedColumn = mapping.generatedColumn;

  	      if (mapping.source != null) {
  	        sourceIdx = this._sources.indexOf(mapping.source);
  	        next += base64VLQ.encode(sourceIdx - previousSource);
  	        previousSource = sourceIdx;

  	        // lines are stored 0-based in SourceMap spec version 3
  	        next += base64VLQ.encode(mapping.originalLine - 1
  	                                   - previousOriginalLine);
  	        previousOriginalLine = mapping.originalLine - 1;

  	        next += base64VLQ.encode(mapping.originalColumn
  	                                   - previousOriginalColumn);
  	        previousOriginalColumn = mapping.originalColumn;

  	        if (mapping.name != null) {
  	          nameIdx = this._names.indexOf(mapping.name);
  	          next += base64VLQ.encode(nameIdx - previousName);
  	          previousName = nameIdx;
  	        }
  	      }

  	      result += next;
  	    }

  	    return result;
  	  };

  	SourceMapGenerator.prototype._generateSourcesContent =
  	  function SourceMapGenerator_generateSourcesContent(aSources, aSourceRoot) {
  	    return aSources.map(function (source) {
  	      if (!this._sourcesContents) {
  	        return null;
  	      }
  	      if (aSourceRoot != null) {
  	        source = util.relative(aSourceRoot, source);
  	      }
  	      var key = util.toSetString(source);
  	      return Object.prototype.hasOwnProperty.call(this._sourcesContents, key)
  	        ? this._sourcesContents[key]
  	        : null;
  	    }, this);
  	  };

  	/**
  	 * Externalize the source map.
  	 */
  	SourceMapGenerator.prototype.toJSON =
  	  function SourceMapGenerator_toJSON() {
  	    var map = {
  	      version: this._version,
  	      sources: this._sources.toArray(),
  	      names: this._names.toArray(),
  	      mappings: this._serializeMappings()
  	    };
  	    if (this._file != null) {
  	      map.file = this._file;
  	    }
  	    if (this._sourceRoot != null) {
  	      map.sourceRoot = this._sourceRoot;
  	    }
  	    if (this._sourcesContents) {
  	      map.sourcesContent = this._generateSourcesContent(map.sources, map.sourceRoot);
  	    }

  	    return map;
  	  };

  	/**
  	 * Render the source map being generated to a string.
  	 */
  	SourceMapGenerator.prototype.toString =
  	  function SourceMapGenerator_toString() {
  	    return JSON.stringify(this.toJSON());
  	  };

  	sourceMapGenerator.SourceMapGenerator = SourceMapGenerator;
  	return sourceMapGenerator;
  }

  var sourceMapConsumer = {};

  var binarySearch = {};

  /* -*- Mode: js; js-indent-level: 2; -*- */

  var hasRequiredBinarySearch;

  function requireBinarySearch () {
  	if (hasRequiredBinarySearch) return binarySearch;
  	hasRequiredBinarySearch = 1;
  	(function (exports$1) {
  		/*
  		 * Copyright 2011 Mozilla Foundation and contributors
  		 * Licensed under the New BSD license. See LICENSE or:
  		 * http://opensource.org/licenses/BSD-3-Clause
  		 */

  		exports$1.GREATEST_LOWER_BOUND = 1;
  		exports$1.LEAST_UPPER_BOUND = 2;

  		/**
  		 * Recursive implementation of binary search.
  		 *
  		 * @param aLow Indices here and lower do not contain the needle.
  		 * @param aHigh Indices here and higher do not contain the needle.
  		 * @param aNeedle The element being searched for.
  		 * @param aHaystack The non-empty array being searched.
  		 * @param aCompare Function which takes two elements and returns -1, 0, or 1.
  		 * @param aBias Either 'binarySearch.GREATEST_LOWER_BOUND' or
  		 *     'binarySearch.LEAST_UPPER_BOUND'. Specifies whether to return the
  		 *     closest element that is smaller than or greater than the one we are
  		 *     searching for, respectively, if the exact element cannot be found.
  		 */
  		function recursiveSearch(aLow, aHigh, aNeedle, aHaystack, aCompare, aBias) {
  		  // This function terminates when one of the following is true:
  		  //
  		  //   1. We find the exact element we are looking for.
  		  //
  		  //   2. We did not find the exact element, but we can return the index of
  		  //      the next-closest element.
  		  //
  		  //   3. We did not find the exact element, and there is no next-closest
  		  //      element than the one we are searching for, so we return -1.
  		  var mid = Math.floor((aHigh - aLow) / 2) + aLow;
  		  var cmp = aCompare(aNeedle, aHaystack[mid], true);
  		  if (cmp === 0) {
  		    // Found the element we are looking for.
  		    return mid;
  		  }
  		  else if (cmp > 0) {
  		    // Our needle is greater than aHaystack[mid].
  		    if (aHigh - mid > 1) {
  		      // The element is in the upper half.
  		      return recursiveSearch(mid, aHigh, aNeedle, aHaystack, aCompare, aBias);
  		    }

  		    // The exact needle element was not found in this haystack. Determine if
  		    // we are in termination case (3) or (2) and return the appropriate thing.
  		    if (aBias == exports$1.LEAST_UPPER_BOUND) {
  		      return aHigh < aHaystack.length ? aHigh : -1;
  		    } else {
  		      return mid;
  		    }
  		  }
  		  else {
  		    // Our needle is less than aHaystack[mid].
  		    if (mid - aLow > 1) {
  		      // The element is in the lower half.
  		      return recursiveSearch(aLow, mid, aNeedle, aHaystack, aCompare, aBias);
  		    }

  		    // we are in termination case (3) or (2) and return the appropriate thing.
  		    if (aBias == exports$1.LEAST_UPPER_BOUND) {
  		      return mid;
  		    } else {
  		      return aLow < 0 ? -1 : aLow;
  		    }
  		  }
  		}

  		/**
  		 * This is an implementation of binary search which will always try and return
  		 * the index of the closest element if there is no exact hit. This is because
  		 * mappings between original and generated line/col pairs are single points,
  		 * and there is an implicit region between each of them, so a miss just means
  		 * that you aren't on the very start of a region.
  		 *
  		 * @param aNeedle The element you are looking for.
  		 * @param aHaystack The array that is being searched.
  		 * @param aCompare A function which takes the needle and an element in the
  		 *     array and returns -1, 0, or 1 depending on whether the needle is less
  		 *     than, equal to, or greater than the element, respectively.
  		 * @param aBias Either 'binarySearch.GREATEST_LOWER_BOUND' or
  		 *     'binarySearch.LEAST_UPPER_BOUND'. Specifies whether to return the
  		 *     closest element that is smaller than or greater than the one we are
  		 *     searching for, respectively, if the exact element cannot be found.
  		 *     Defaults to 'binarySearch.GREATEST_LOWER_BOUND'.
  		 */
  		exports$1.search = function search(aNeedle, aHaystack, aCompare, aBias) {
  		  if (aHaystack.length === 0) {
  		    return -1;
  		  }

  		  var index = recursiveSearch(-1, aHaystack.length, aNeedle, aHaystack,
  		                              aCompare, aBias || exports$1.GREATEST_LOWER_BOUND);
  		  if (index < 0) {
  		    return -1;
  		  }

  		  // We have found either the exact element, or the next-closest element than
  		  // the one we are searching for. However, there may be more than one such
  		  // element. Make sure we always return the smallest of these.
  		  while (index - 1 >= 0) {
  		    if (aCompare(aHaystack[index], aHaystack[index - 1], true) !== 0) {
  		      break;
  		    }
  		    --index;
  		  }

  		  return index;
  		}; 
  	} (binarySearch));
  	return binarySearch;
  }

  var quickSort = {};

  /* -*- Mode: js; js-indent-level: 2; -*- */

  var hasRequiredQuickSort;

  function requireQuickSort () {
  	if (hasRequiredQuickSort) return quickSort;
  	hasRequiredQuickSort = 1;
  	/*
  	 * Copyright 2011 Mozilla Foundation and contributors
  	 * Licensed under the New BSD license. See LICENSE or:
  	 * http://opensource.org/licenses/BSD-3-Clause
  	 */

  	// It turns out that some (most?) JavaScript engines don't self-host
  	// `Array.prototype.sort`. This makes sense because C++ will likely remain
  	// faster than JS when doing raw CPU-intensive sorting. However, when using a
  	// custom comparator function, calling back and forth between the VM's C++ and
  	// JIT'd JS is rather slow *and* loses JIT type information, resulting in
  	// worse generated code for the comparator function than would be optimal. In
  	// fact, when sorting with a comparator, these costs outweigh the benefits of
  	// sorting in C++. By using our own JS-implemented Quick Sort (below), we get
  	// a ~3500ms mean speed-up in `bench/bench.html`.

  	/**
  	 * Swap the elements indexed by `x` and `y` in the array `ary`.
  	 *
  	 * @param {Array} ary
  	 *        The array.
  	 * @param {Number} x
  	 *        The index of the first item.
  	 * @param {Number} y
  	 *        The index of the second item.
  	 */
  	function swap(ary, x, y) {
  	  var temp = ary[x];
  	  ary[x] = ary[y];
  	  ary[y] = temp;
  	}

  	/**
  	 * Returns a random integer within the range `low .. high` inclusive.
  	 *
  	 * @param {Number} low
  	 *        The lower bound on the range.
  	 * @param {Number} high
  	 *        The upper bound on the range.
  	 */
  	function randomIntInRange(low, high) {
  	  return Math.round(low + (Math.random() * (high - low)));
  	}

  	/**
  	 * The Quick Sort algorithm.
  	 *
  	 * @param {Array} ary
  	 *        An array to sort.
  	 * @param {function} comparator
  	 *        Function to use to compare two items.
  	 * @param {Number} p
  	 *        Start index of the array
  	 * @param {Number} r
  	 *        End index of the array
  	 */
  	function doQuickSort(ary, comparator, p, r) {
  	  // If our lower bound is less than our upper bound, we (1) partition the
  	  // array into two pieces and (2) recurse on each half. If it is not, this is
  	  // the empty array and our base case.

  	  if (p < r) {
  	    // (1) Partitioning.
  	    //
  	    // The partitioning chooses a pivot between `p` and `r` and moves all
  	    // elements that are less than or equal to the pivot to the before it, and
  	    // all the elements that are greater than it after it. The effect is that
  	    // once partition is done, the pivot is in the exact place it will be when
  	    // the array is put in sorted order, and it will not need to be moved
  	    // again. This runs in O(n) time.

  	    // Always choose a random pivot so that an input array which is reverse
  	    // sorted does not cause O(n^2) running time.
  	    var pivotIndex = randomIntInRange(p, r);
  	    var i = p - 1;

  	    swap(ary, pivotIndex, r);
  	    var pivot = ary[r];

  	    // Immediately after `j` is incremented in this loop, the following hold
  	    // true:
  	    //
  	    //   * Every element in `ary[p .. i]` is less than or equal to the pivot.
  	    //
  	    //   * Every element in `ary[i+1 .. j-1]` is greater than the pivot.
  	    for (var j = p; j < r; j++) {
  	      if (comparator(ary[j], pivot) <= 0) {
  	        i += 1;
  	        swap(ary, i, j);
  	      }
  	    }

  	    swap(ary, i + 1, j);
  	    var q = i + 1;

  	    // (2) Recurse on each half.

  	    doQuickSort(ary, comparator, p, q - 1);
  	    doQuickSort(ary, comparator, q + 1, r);
  	  }
  	}

  	/**
  	 * Sort the given array in-place with the given comparator function.
  	 *
  	 * @param {Array} ary
  	 *        An array to sort.
  	 * @param {function} comparator
  	 *        Function to use to compare two items.
  	 */
  	quickSort.quickSort = function (ary, comparator) {
  	  doQuickSort(ary, comparator, 0, ary.length - 1);
  	};
  	return quickSort;
  }

  /* -*- Mode: js; js-indent-level: 2; -*- */

  var hasRequiredSourceMapConsumer;

  function requireSourceMapConsumer () {
  	if (hasRequiredSourceMapConsumer) return sourceMapConsumer;
  	hasRequiredSourceMapConsumer = 1;
  	/*
  	 * Copyright 2011 Mozilla Foundation and contributors
  	 * Licensed under the New BSD license. See LICENSE or:
  	 * http://opensource.org/licenses/BSD-3-Clause
  	 */

  	var util = requireUtil();
  	var binarySearch = requireBinarySearch();
  	var ArraySet = requireArraySet().ArraySet;
  	var base64VLQ = requireBase64Vlq();
  	var quickSort = requireQuickSort().quickSort;

  	function SourceMapConsumer(aSourceMap, aSourceMapURL) {
  	  var sourceMap = aSourceMap;
  	  if (typeof aSourceMap === 'string') {
  	    sourceMap = util.parseSourceMapInput(aSourceMap);
  	  }

  	  return sourceMap.sections != null
  	    ? new IndexedSourceMapConsumer(sourceMap, aSourceMapURL)
  	    : new BasicSourceMapConsumer(sourceMap, aSourceMapURL);
  	}

  	SourceMapConsumer.fromSourceMap = function(aSourceMap, aSourceMapURL) {
  	  return BasicSourceMapConsumer.fromSourceMap(aSourceMap, aSourceMapURL);
  	};

  	/**
  	 * The version of the source mapping spec that we are consuming.
  	 */
  	SourceMapConsumer.prototype._version = 3;

  	// `__generatedMappings` and `__originalMappings` are arrays that hold the
  	// parsed mapping coordinates from the source map's "mappings" attribute. They
  	// are lazily instantiated, accessed via the `_generatedMappings` and
  	// `_originalMappings` getters respectively, and we only parse the mappings
  	// and create these arrays once queried for a source location. We jump through
  	// these hoops because there can be many thousands of mappings, and parsing
  	// them is expensive, so we only want to do it if we must.
  	//
  	// Each object in the arrays is of the form:
  	//
  	//     {
  	//       generatedLine: The line number in the generated code,
  	//       generatedColumn: The column number in the generated code,
  	//       source: The path to the original source file that generated this
  	//               chunk of code,
  	//       originalLine: The line number in the original source that
  	//                     corresponds to this chunk of generated code,
  	//       originalColumn: The column number in the original source that
  	//                       corresponds to this chunk of generated code,
  	//       name: The name of the original symbol which generated this chunk of
  	//             code.
  	//     }
  	//
  	// All properties except for `generatedLine` and `generatedColumn` can be
  	// `null`.
  	//
  	// `_generatedMappings` is ordered by the generated positions.
  	//
  	// `_originalMappings` is ordered by the original positions.

  	SourceMapConsumer.prototype.__generatedMappings = null;
  	Object.defineProperty(SourceMapConsumer.prototype, '_generatedMappings', {
  	  configurable: true,
  	  enumerable: true,
  	  get: function () {
  	    if (!this.__generatedMappings) {
  	      this._parseMappings(this._mappings, this.sourceRoot);
  	    }

  	    return this.__generatedMappings;
  	  }
  	});

  	SourceMapConsumer.prototype.__originalMappings = null;
  	Object.defineProperty(SourceMapConsumer.prototype, '_originalMappings', {
  	  configurable: true,
  	  enumerable: true,
  	  get: function () {
  	    if (!this.__originalMappings) {
  	      this._parseMappings(this._mappings, this.sourceRoot);
  	    }

  	    return this.__originalMappings;
  	  }
  	});

  	SourceMapConsumer.prototype._charIsMappingSeparator =
  	  function SourceMapConsumer_charIsMappingSeparator(aStr, index) {
  	    var c = aStr.charAt(index);
  	    return c === ";" || c === ",";
  	  };

  	/**
  	 * Parse the mappings in a string in to a data structure which we can easily
  	 * query (the ordered arrays in the `this.__generatedMappings` and
  	 * `this.__originalMappings` properties).
  	 */
  	SourceMapConsumer.prototype._parseMappings =
  	  function SourceMapConsumer_parseMappings(aStr, aSourceRoot) {
  	    throw new Error("Subclasses must implement _parseMappings");
  	  };

  	SourceMapConsumer.GENERATED_ORDER = 1;
  	SourceMapConsumer.ORIGINAL_ORDER = 2;

  	SourceMapConsumer.GREATEST_LOWER_BOUND = 1;
  	SourceMapConsumer.LEAST_UPPER_BOUND = 2;

  	/**
  	 * Iterate over each mapping between an original source/line/column and a
  	 * generated line/column in this source map.
  	 *
  	 * @param Function aCallback
  	 *        The function that is called with each mapping.
  	 * @param Object aContext
  	 *        Optional. If specified, this object will be the value of `this` every
  	 *        time that `aCallback` is called.
  	 * @param aOrder
  	 *        Either `SourceMapConsumer.GENERATED_ORDER` or
  	 *        `SourceMapConsumer.ORIGINAL_ORDER`. Specifies whether you want to
  	 *        iterate over the mappings sorted by the generated file's line/column
  	 *        order or the original's source/line/column order, respectively. Defaults to
  	 *        `SourceMapConsumer.GENERATED_ORDER`.
  	 */
  	SourceMapConsumer.prototype.eachMapping =
  	  function SourceMapConsumer_eachMapping(aCallback, aContext, aOrder) {
  	    var context = aContext || null;
  	    var order = aOrder || SourceMapConsumer.GENERATED_ORDER;

  	    var mappings;
  	    switch (order) {
  	    case SourceMapConsumer.GENERATED_ORDER:
  	      mappings = this._generatedMappings;
  	      break;
  	    case SourceMapConsumer.ORIGINAL_ORDER:
  	      mappings = this._originalMappings;
  	      break;
  	    default:
  	      throw new Error("Unknown order of iteration.");
  	    }

  	    var sourceRoot = this.sourceRoot;
  	    mappings.map(function (mapping) {
  	      var source = mapping.source === null ? null : this._sources.at(mapping.source);
  	      source = util.computeSourceURL(sourceRoot, source, this._sourceMapURL);
  	      return {
  	        source: source,
  	        generatedLine: mapping.generatedLine,
  	        generatedColumn: mapping.generatedColumn,
  	        originalLine: mapping.originalLine,
  	        originalColumn: mapping.originalColumn,
  	        name: mapping.name === null ? null : this._names.at(mapping.name)
  	      };
  	    }, this).forEach(aCallback, context);
  	  };

  	/**
  	 * Returns all generated line and column information for the original source,
  	 * line, and column provided. If no column is provided, returns all mappings
  	 * corresponding to a either the line we are searching for or the next
  	 * closest line that has any mappings. Otherwise, returns all mappings
  	 * corresponding to the given line and either the column we are searching for
  	 * or the next closest column that has any offsets.
  	 *
  	 * The only argument is an object with the following properties:
  	 *
  	 *   - source: The filename of the original source.
  	 *   - line: The line number in the original source.  The line number is 1-based.
  	 *   - column: Optional. the column number in the original source.
  	 *    The column number is 0-based.
  	 *
  	 * and an array of objects is returned, each with the following properties:
  	 *
  	 *   - line: The line number in the generated source, or null.  The
  	 *    line number is 1-based.
  	 *   - column: The column number in the generated source, or null.
  	 *    The column number is 0-based.
  	 */
  	SourceMapConsumer.prototype.allGeneratedPositionsFor =
  	  function SourceMapConsumer_allGeneratedPositionsFor(aArgs) {
  	    var line = util.getArg(aArgs, 'line');

  	    // When there is no exact match, BasicSourceMapConsumer.prototype._findMapping
  	    // returns the index of the closest mapping less than the needle. By
  	    // setting needle.originalColumn to 0, we thus find the last mapping for
  	    // the given line, provided such a mapping exists.
  	    var needle = {
  	      source: util.getArg(aArgs, 'source'),
  	      originalLine: line,
  	      originalColumn: util.getArg(aArgs, 'column', 0)
  	    };

  	    needle.source = this._findSourceIndex(needle.source);
  	    if (needle.source < 0) {
  	      return [];
  	    }

  	    var mappings = [];

  	    var index = this._findMapping(needle,
  	                                  this._originalMappings,
  	                                  "originalLine",
  	                                  "originalColumn",
  	                                  util.compareByOriginalPositions,
  	                                  binarySearch.LEAST_UPPER_BOUND);
  	    if (index >= 0) {
  	      var mapping = this._originalMappings[index];

  	      if (aArgs.column === undefined) {
  	        var originalLine = mapping.originalLine;

  	        // Iterate until either we run out of mappings, or we run into
  	        // a mapping for a different line than the one we found. Since
  	        // mappings are sorted, this is guaranteed to find all mappings for
  	        // the line we found.
  	        while (mapping && mapping.originalLine === originalLine) {
  	          mappings.push({
  	            line: util.getArg(mapping, 'generatedLine', null),
  	            column: util.getArg(mapping, 'generatedColumn', null),
  	            lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
  	          });

  	          mapping = this._originalMappings[++index];
  	        }
  	      } else {
  	        var originalColumn = mapping.originalColumn;

  	        // Iterate until either we run out of mappings, or we run into
  	        // a mapping for a different line than the one we were searching for.
  	        // Since mappings are sorted, this is guaranteed to find all mappings for
  	        // the line we are searching for.
  	        while (mapping &&
  	               mapping.originalLine === line &&
  	               mapping.originalColumn == originalColumn) {
  	          mappings.push({
  	            line: util.getArg(mapping, 'generatedLine', null),
  	            column: util.getArg(mapping, 'generatedColumn', null),
  	            lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
  	          });

  	          mapping = this._originalMappings[++index];
  	        }
  	      }
  	    }

  	    return mappings;
  	  };

  	sourceMapConsumer.SourceMapConsumer = SourceMapConsumer;

  	/**
  	 * A BasicSourceMapConsumer instance represents a parsed source map which we can
  	 * query for information about the original file positions by giving it a file
  	 * position in the generated source.
  	 *
  	 * The first parameter is the raw source map (either as a JSON string, or
  	 * already parsed to an object). According to the spec, source maps have the
  	 * following attributes:
  	 *
  	 *   - version: Which version of the source map spec this map is following.
  	 *   - sources: An array of URLs to the original source files.
  	 *   - names: An array of identifiers which can be referrenced by individual mappings.
  	 *   - sourceRoot: Optional. The URL root from which all sources are relative.
  	 *   - sourcesContent: Optional. An array of contents of the original source files.
  	 *   - mappings: A string of base64 VLQs which contain the actual mappings.
  	 *   - file: Optional. The generated file this source map is associated with.
  	 *
  	 * Here is an example source map, taken from the source map spec[0]:
  	 *
  	 *     {
  	 *       version : 3,
  	 *       file: "out.js",
  	 *       sourceRoot : "",
  	 *       sources: ["foo.js", "bar.js"],
  	 *       names: ["src", "maps", "are", "fun"],
  	 *       mappings: "AA,AB;;ABCDE;"
  	 *     }
  	 *
  	 * The second parameter, if given, is a string whose value is the URL
  	 * at which the source map was found.  This URL is used to compute the
  	 * sources array.
  	 *
  	 * [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?pli=1#
  	 */
  	function BasicSourceMapConsumer(aSourceMap, aSourceMapURL) {
  	  var sourceMap = aSourceMap;
  	  if (typeof aSourceMap === 'string') {
  	    sourceMap = util.parseSourceMapInput(aSourceMap);
  	  }

  	  var version = util.getArg(sourceMap, 'version');
  	  var sources = util.getArg(sourceMap, 'sources');
  	  // Sass 3.3 leaves out the 'names' array, so we deviate from the spec (which
  	  // requires the array) to play nice here.
  	  var names = util.getArg(sourceMap, 'names', []);
  	  var sourceRoot = util.getArg(sourceMap, 'sourceRoot', null);
  	  var sourcesContent = util.getArg(sourceMap, 'sourcesContent', null);
  	  var mappings = util.getArg(sourceMap, 'mappings');
  	  var file = util.getArg(sourceMap, 'file', null);

  	  // Once again, Sass deviates from the spec and supplies the version as a
  	  // string rather than a number, so we use loose equality checking here.
  	  if (version != this._version) {
  	    throw new Error('Unsupported version: ' + version);
  	  }

  	  if (sourceRoot) {
  	    sourceRoot = util.normalize(sourceRoot);
  	  }

  	  sources = sources
  	    .map(String)
  	    // Some source maps produce relative source paths like "./foo.js" instead of
  	    // "foo.js".  Normalize these first so that future comparisons will succeed.
  	    // See bugzil.la/1090768.
  	    .map(util.normalize)
  	    // Always ensure that absolute sources are internally stored relative to
  	    // the source root, if the source root is absolute. Not doing this would
  	    // be particularly problematic when the source root is a prefix of the
  	    // source (valid, but why??). See github issue #199 and bugzil.la/1188982.
  	    .map(function (source) {
  	      return sourceRoot && util.isAbsolute(sourceRoot) && util.isAbsolute(source)
  	        ? util.relative(sourceRoot, source)
  	        : source;
  	    });

  	  // Pass `true` below to allow duplicate names and sources. While source maps
  	  // are intended to be compressed and deduplicated, the TypeScript compiler
  	  // sometimes generates source maps with duplicates in them. See Github issue
  	  // #72 and bugzil.la/889492.
  	  this._names = ArraySet.fromArray(names.map(String), true);
  	  this._sources = ArraySet.fromArray(sources, true);

  	  this._absoluteSources = this._sources.toArray().map(function (s) {
  	    return util.computeSourceURL(sourceRoot, s, aSourceMapURL);
  	  });

  	  this.sourceRoot = sourceRoot;
  	  this.sourcesContent = sourcesContent;
  	  this._mappings = mappings;
  	  this._sourceMapURL = aSourceMapURL;
  	  this.file = file;
  	}

  	BasicSourceMapConsumer.prototype = Object.create(SourceMapConsumer.prototype);
  	BasicSourceMapConsumer.prototype.consumer = SourceMapConsumer;

  	/**
  	 * Utility function to find the index of a source.  Returns -1 if not
  	 * found.
  	 */
  	BasicSourceMapConsumer.prototype._findSourceIndex = function(aSource) {
  	  var relativeSource = aSource;
  	  if (this.sourceRoot != null) {
  	    relativeSource = util.relative(this.sourceRoot, relativeSource);
  	  }

  	  if (this._sources.has(relativeSource)) {
  	    return this._sources.indexOf(relativeSource);
  	  }

  	  // Maybe aSource is an absolute URL as returned by |sources|.  In
  	  // this case we can't simply undo the transform.
  	  var i;
  	  for (i = 0; i < this._absoluteSources.length; ++i) {
  	    if (this._absoluteSources[i] == aSource) {
  	      return i;
  	    }
  	  }

  	  return -1;
  	};

  	/**
  	 * Create a BasicSourceMapConsumer from a SourceMapGenerator.
  	 *
  	 * @param SourceMapGenerator aSourceMap
  	 *        The source map that will be consumed.
  	 * @param String aSourceMapURL
  	 *        The URL at which the source map can be found (optional)
  	 * @returns BasicSourceMapConsumer
  	 */
  	BasicSourceMapConsumer.fromSourceMap =
  	  function SourceMapConsumer_fromSourceMap(aSourceMap, aSourceMapURL) {
  	    var smc = Object.create(BasicSourceMapConsumer.prototype);

  	    var names = smc._names = ArraySet.fromArray(aSourceMap._names.toArray(), true);
  	    var sources = smc._sources = ArraySet.fromArray(aSourceMap._sources.toArray(), true);
  	    smc.sourceRoot = aSourceMap._sourceRoot;
  	    smc.sourcesContent = aSourceMap._generateSourcesContent(smc._sources.toArray(),
  	                                                            smc.sourceRoot);
  	    smc.file = aSourceMap._file;
  	    smc._sourceMapURL = aSourceMapURL;
  	    smc._absoluteSources = smc._sources.toArray().map(function (s) {
  	      return util.computeSourceURL(smc.sourceRoot, s, aSourceMapURL);
  	    });

  	    // Because we are modifying the entries (by converting string sources and
  	    // names to indices into the sources and names ArraySets), we have to make
  	    // a copy of the entry or else bad things happen. Shared mutable state
  	    // strikes again! See github issue #191.

  	    var generatedMappings = aSourceMap._mappings.toArray().slice();
  	    var destGeneratedMappings = smc.__generatedMappings = [];
  	    var destOriginalMappings = smc.__originalMappings = [];

  	    for (var i = 0, length = generatedMappings.length; i < length; i++) {
  	      var srcMapping = generatedMappings[i];
  	      var destMapping = new Mapping;
  	      destMapping.generatedLine = srcMapping.generatedLine;
  	      destMapping.generatedColumn = srcMapping.generatedColumn;

  	      if (srcMapping.source) {
  	        destMapping.source = sources.indexOf(srcMapping.source);
  	        destMapping.originalLine = srcMapping.originalLine;
  	        destMapping.originalColumn = srcMapping.originalColumn;

  	        if (srcMapping.name) {
  	          destMapping.name = names.indexOf(srcMapping.name);
  	        }

  	        destOriginalMappings.push(destMapping);
  	      }

  	      destGeneratedMappings.push(destMapping);
  	    }

  	    quickSort(smc.__originalMappings, util.compareByOriginalPositions);

  	    return smc;
  	  };

  	/**
  	 * The version of the source mapping spec that we are consuming.
  	 */
  	BasicSourceMapConsumer.prototype._version = 3;

  	/**
  	 * The list of original sources.
  	 */
  	Object.defineProperty(BasicSourceMapConsumer.prototype, 'sources', {
  	  get: function () {
  	    return this._absoluteSources.slice();
  	  }
  	});

  	/**
  	 * Provide the JIT with a nice shape / hidden class.
  	 */
  	function Mapping() {
  	  this.generatedLine = 0;
  	  this.generatedColumn = 0;
  	  this.source = null;
  	  this.originalLine = null;
  	  this.originalColumn = null;
  	  this.name = null;
  	}

  	/**
  	 * Parse the mappings in a string in to a data structure which we can easily
  	 * query (the ordered arrays in the `this.__generatedMappings` and
  	 * `this.__originalMappings` properties).
  	 */
  	BasicSourceMapConsumer.prototype._parseMappings =
  	  function SourceMapConsumer_parseMappings(aStr, aSourceRoot) {
  	    var generatedLine = 1;
  	    var previousGeneratedColumn = 0;
  	    var previousOriginalLine = 0;
  	    var previousOriginalColumn = 0;
  	    var previousSource = 0;
  	    var previousName = 0;
  	    var length = aStr.length;
  	    var index = 0;
  	    var cachedSegments = {};
  	    var temp = {};
  	    var originalMappings = [];
  	    var generatedMappings = [];
  	    var mapping, str, segment, end, value;

  	    while (index < length) {
  	      if (aStr.charAt(index) === ';') {
  	        generatedLine++;
  	        index++;
  	        previousGeneratedColumn = 0;
  	      }
  	      else if (aStr.charAt(index) === ',') {
  	        index++;
  	      }
  	      else {
  	        mapping = new Mapping();
  	        mapping.generatedLine = generatedLine;

  	        // Because each offset is encoded relative to the previous one,
  	        // many segments often have the same encoding. We can exploit this
  	        // fact by caching the parsed variable length fields of each segment,
  	        // allowing us to avoid a second parse if we encounter the same
  	        // segment again.
  	        for (end = index; end < length; end++) {
  	          if (this._charIsMappingSeparator(aStr, end)) {
  	            break;
  	          }
  	        }
  	        str = aStr.slice(index, end);

  	        segment = cachedSegments[str];
  	        if (segment) {
  	          index += str.length;
  	        } else {
  	          segment = [];
  	          while (index < end) {
  	            base64VLQ.decode(aStr, index, temp);
  	            value = temp.value;
  	            index = temp.rest;
  	            segment.push(value);
  	          }

  	          if (segment.length === 2) {
  	            throw new Error('Found a source, but no line and column');
  	          }

  	          if (segment.length === 3) {
  	            throw new Error('Found a source and line, but no column');
  	          }

  	          cachedSegments[str] = segment;
  	        }

  	        // Generated column.
  	        mapping.generatedColumn = previousGeneratedColumn + segment[0];
  	        previousGeneratedColumn = mapping.generatedColumn;

  	        if (segment.length > 1) {
  	          // Original source.
  	          mapping.source = previousSource + segment[1];
  	          previousSource += segment[1];

  	          // Original line.
  	          mapping.originalLine = previousOriginalLine + segment[2];
  	          previousOriginalLine = mapping.originalLine;
  	          // Lines are stored 0-based
  	          mapping.originalLine += 1;

  	          // Original column.
  	          mapping.originalColumn = previousOriginalColumn + segment[3];
  	          previousOriginalColumn = mapping.originalColumn;

  	          if (segment.length > 4) {
  	            // Original name.
  	            mapping.name = previousName + segment[4];
  	            previousName += segment[4];
  	          }
  	        }

  	        generatedMappings.push(mapping);
  	        if (typeof mapping.originalLine === 'number') {
  	          originalMappings.push(mapping);
  	        }
  	      }
  	    }

  	    quickSort(generatedMappings, util.compareByGeneratedPositionsDeflated);
  	    this.__generatedMappings = generatedMappings;

  	    quickSort(originalMappings, util.compareByOriginalPositions);
  	    this.__originalMappings = originalMappings;
  	  };

  	/**
  	 * Find the mapping that best matches the hypothetical "needle" mapping that
  	 * we are searching for in the given "haystack" of mappings.
  	 */
  	BasicSourceMapConsumer.prototype._findMapping =
  	  function SourceMapConsumer_findMapping(aNeedle, aMappings, aLineName,
  	                                         aColumnName, aComparator, aBias) {
  	    // To return the position we are searching for, we must first find the
  	    // mapping for the given position and then return the opposite position it
  	    // points to. Because the mappings are sorted, we can use binary search to
  	    // find the best mapping.

  	    if (aNeedle[aLineName] <= 0) {
  	      throw new TypeError('Line must be greater than or equal to 1, got '
  	                          + aNeedle[aLineName]);
  	    }
  	    if (aNeedle[aColumnName] < 0) {
  	      throw new TypeError('Column must be greater than or equal to 0, got '
  	                          + aNeedle[aColumnName]);
  	    }

  	    return binarySearch.search(aNeedle, aMappings, aComparator, aBias);
  	  };

  	/**
  	 * Compute the last column for each generated mapping. The last column is
  	 * inclusive.
  	 */
  	BasicSourceMapConsumer.prototype.computeColumnSpans =
  	  function SourceMapConsumer_computeColumnSpans() {
  	    for (var index = 0; index < this._generatedMappings.length; ++index) {
  	      var mapping = this._generatedMappings[index];

  	      // Mappings do not contain a field for the last generated columnt. We
  	      // can come up with an optimistic estimate, however, by assuming that
  	      // mappings are contiguous (i.e. given two consecutive mappings, the
  	      // first mapping ends where the second one starts).
  	      if (index + 1 < this._generatedMappings.length) {
  	        var nextMapping = this._generatedMappings[index + 1];

  	        if (mapping.generatedLine === nextMapping.generatedLine) {
  	          mapping.lastGeneratedColumn = nextMapping.generatedColumn - 1;
  	          continue;
  	        }
  	      }

  	      // The last mapping for each line spans the entire line.
  	      mapping.lastGeneratedColumn = Infinity;
  	    }
  	  };

  	/**
  	 * Returns the original source, line, and column information for the generated
  	 * source's line and column positions provided. The only argument is an object
  	 * with the following properties:
  	 *
  	 *   - line: The line number in the generated source.  The line number
  	 *     is 1-based.
  	 *   - column: The column number in the generated source.  The column
  	 *     number is 0-based.
  	 *   - bias: Either 'SourceMapConsumer.GREATEST_LOWER_BOUND' or
  	 *     'SourceMapConsumer.LEAST_UPPER_BOUND'. Specifies whether to return the
  	 *     closest element that is smaller than or greater than the one we are
  	 *     searching for, respectively, if the exact element cannot be found.
  	 *     Defaults to 'SourceMapConsumer.GREATEST_LOWER_BOUND'.
  	 *
  	 * and an object is returned with the following properties:
  	 *
  	 *   - source: The original source file, or null.
  	 *   - line: The line number in the original source, or null.  The
  	 *     line number is 1-based.
  	 *   - column: The column number in the original source, or null.  The
  	 *     column number is 0-based.
  	 *   - name: The original identifier, or null.
  	 */
  	BasicSourceMapConsumer.prototype.originalPositionFor =
  	  function SourceMapConsumer_originalPositionFor(aArgs) {
  	    var needle = {
  	      generatedLine: util.getArg(aArgs, 'line'),
  	      generatedColumn: util.getArg(aArgs, 'column')
  	    };

  	    var index = this._findMapping(
  	      needle,
  	      this._generatedMappings,
  	      "generatedLine",
  	      "generatedColumn",
  	      util.compareByGeneratedPositionsDeflated,
  	      util.getArg(aArgs, 'bias', SourceMapConsumer.GREATEST_LOWER_BOUND)
  	    );

  	    if (index >= 0) {
  	      var mapping = this._generatedMappings[index];

  	      if (mapping.generatedLine === needle.generatedLine) {
  	        var source = util.getArg(mapping, 'source', null);
  	        if (source !== null) {
  	          source = this._sources.at(source);
  	          source = util.computeSourceURL(this.sourceRoot, source, this._sourceMapURL);
  	        }
  	        var name = util.getArg(mapping, 'name', null);
  	        if (name !== null) {
  	          name = this._names.at(name);
  	        }
  	        return {
  	          source: source,
  	          line: util.getArg(mapping, 'originalLine', null),
  	          column: util.getArg(mapping, 'originalColumn', null),
  	          name: name
  	        };
  	      }
  	    }

  	    return {
  	      source: null,
  	      line: null,
  	      column: null,
  	      name: null
  	    };
  	  };

  	/**
  	 * Return true if we have the source content for every source in the source
  	 * map, false otherwise.
  	 */
  	BasicSourceMapConsumer.prototype.hasContentsOfAllSources =
  	  function BasicSourceMapConsumer_hasContentsOfAllSources() {
  	    if (!this.sourcesContent) {
  	      return false;
  	    }
  	    return this.sourcesContent.length >= this._sources.size() &&
  	      !this.sourcesContent.some(function (sc) { return sc == null; });
  	  };

  	/**
  	 * Returns the original source content. The only argument is the url of the
  	 * original source file. Returns null if no original source content is
  	 * available.
  	 */
  	BasicSourceMapConsumer.prototype.sourceContentFor =
  	  function SourceMapConsumer_sourceContentFor(aSource, nullOnMissing) {
  	    if (!this.sourcesContent) {
  	      return null;
  	    }

  	    var index = this._findSourceIndex(aSource);
  	    if (index >= 0) {
  	      return this.sourcesContent[index];
  	    }

  	    var relativeSource = aSource;
  	    if (this.sourceRoot != null) {
  	      relativeSource = util.relative(this.sourceRoot, relativeSource);
  	    }

  	    var url;
  	    if (this.sourceRoot != null
  	        && (url = util.urlParse(this.sourceRoot))) {
  	      // XXX: file:// URIs and absolute paths lead to unexpected behavior for
  	      // many users. We can help them out when they expect file:// URIs to
  	      // behave like it would if they were running a local HTTP server. See
  	      // https://bugzilla.mozilla.org/show_bug.cgi?id=885597.
  	      var fileUriAbsPath = relativeSource.replace(/^file:\/\//, "");
  	      if (url.scheme == "file"
  	          && this._sources.has(fileUriAbsPath)) {
  	        return this.sourcesContent[this._sources.indexOf(fileUriAbsPath)]
  	      }

  	      if ((!url.path || url.path == "/")
  	          && this._sources.has("/" + relativeSource)) {
  	        return this.sourcesContent[this._sources.indexOf("/" + relativeSource)];
  	      }
  	    }

  	    // This function is used recursively from
  	    // IndexedSourceMapConsumer.prototype.sourceContentFor. In that case, we
  	    // don't want to throw if we can't find the source - we just want to
  	    // return null, so we provide a flag to exit gracefully.
  	    if (nullOnMissing) {
  	      return null;
  	    }
  	    else {
  	      throw new Error('"' + relativeSource + '" is not in the SourceMap.');
  	    }
  	  };

  	/**
  	 * Returns the generated line and column information for the original source,
  	 * line, and column positions provided. The only argument is an object with
  	 * the following properties:
  	 *
  	 *   - source: The filename of the original source.
  	 *   - line: The line number in the original source.  The line number
  	 *     is 1-based.
  	 *   - column: The column number in the original source.  The column
  	 *     number is 0-based.
  	 *   - bias: Either 'SourceMapConsumer.GREATEST_LOWER_BOUND' or
  	 *     'SourceMapConsumer.LEAST_UPPER_BOUND'. Specifies whether to return the
  	 *     closest element that is smaller than or greater than the one we are
  	 *     searching for, respectively, if the exact element cannot be found.
  	 *     Defaults to 'SourceMapConsumer.GREATEST_LOWER_BOUND'.
  	 *
  	 * and an object is returned with the following properties:
  	 *
  	 *   - line: The line number in the generated source, or null.  The
  	 *     line number is 1-based.
  	 *   - column: The column number in the generated source, or null.
  	 *     The column number is 0-based.
  	 */
  	BasicSourceMapConsumer.prototype.generatedPositionFor =
  	  function SourceMapConsumer_generatedPositionFor(aArgs) {
  	    var source = util.getArg(aArgs, 'source');
  	    source = this._findSourceIndex(source);
  	    if (source < 0) {
  	      return {
  	        line: null,
  	        column: null,
  	        lastColumn: null
  	      };
  	    }

  	    var needle = {
  	      source: source,
  	      originalLine: util.getArg(aArgs, 'line'),
  	      originalColumn: util.getArg(aArgs, 'column')
  	    };

  	    var index = this._findMapping(
  	      needle,
  	      this._originalMappings,
  	      "originalLine",
  	      "originalColumn",
  	      util.compareByOriginalPositions,
  	      util.getArg(aArgs, 'bias', SourceMapConsumer.GREATEST_LOWER_BOUND)
  	    );

  	    if (index >= 0) {
  	      var mapping = this._originalMappings[index];

  	      if (mapping.source === needle.source) {
  	        return {
  	          line: util.getArg(mapping, 'generatedLine', null),
  	          column: util.getArg(mapping, 'generatedColumn', null),
  	          lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
  	        };
  	      }
  	    }

  	    return {
  	      line: null,
  	      column: null,
  	      lastColumn: null
  	    };
  	  };

  	sourceMapConsumer.BasicSourceMapConsumer = BasicSourceMapConsumer;

  	/**
  	 * An IndexedSourceMapConsumer instance represents a parsed source map which
  	 * we can query for information. It differs from BasicSourceMapConsumer in
  	 * that it takes "indexed" source maps (i.e. ones with a "sections" field) as
  	 * input.
  	 *
  	 * The first parameter is a raw source map (either as a JSON string, or already
  	 * parsed to an object). According to the spec for indexed source maps, they
  	 * have the following attributes:
  	 *
  	 *   - version: Which version of the source map spec this map is following.
  	 *   - file: Optional. The generated file this source map is associated with.
  	 *   - sections: A list of section definitions.
  	 *
  	 * Each value under the "sections" field has two fields:
  	 *   - offset: The offset into the original specified at which this section
  	 *       begins to apply, defined as an object with a "line" and "column"
  	 *       field.
  	 *   - map: A source map definition. This source map could also be indexed,
  	 *       but doesn't have to be.
  	 *
  	 * Instead of the "map" field, it's also possible to have a "url" field
  	 * specifying a URL to retrieve a source map from, but that's currently
  	 * unsupported.
  	 *
  	 * Here's an example source map, taken from the source map spec[0], but
  	 * modified to omit a section which uses the "url" field.
  	 *
  	 *  {
  	 *    version : 3,
  	 *    file: "app.js",
  	 *    sections: [{
  	 *      offset: {line:100, column:10},
  	 *      map: {
  	 *        version : 3,
  	 *        file: "section.js",
  	 *        sources: ["foo.js", "bar.js"],
  	 *        names: ["src", "maps", "are", "fun"],
  	 *        mappings: "AAAA,E;;ABCDE;"
  	 *      }
  	 *    }],
  	 *  }
  	 *
  	 * The second parameter, if given, is a string whose value is the URL
  	 * at which the source map was found.  This URL is used to compute the
  	 * sources array.
  	 *
  	 * [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#heading=h.535es3xeprgt
  	 */
  	function IndexedSourceMapConsumer(aSourceMap, aSourceMapURL) {
  	  var sourceMap = aSourceMap;
  	  if (typeof aSourceMap === 'string') {
  	    sourceMap = util.parseSourceMapInput(aSourceMap);
  	  }

  	  var version = util.getArg(sourceMap, 'version');
  	  var sections = util.getArg(sourceMap, 'sections');

  	  if (version != this._version) {
  	    throw new Error('Unsupported version: ' + version);
  	  }

  	  this._sources = new ArraySet();
  	  this._names = new ArraySet();

  	  var lastOffset = {
  	    line: -1,
  	    column: 0
  	  };
  	  this._sections = sections.map(function (s) {
  	    if (s.url) {
  	      // The url field will require support for asynchronicity.
  	      // See https://github.com/mozilla/source-map/issues/16
  	      throw new Error('Support for url field in sections not implemented.');
  	    }
  	    var offset = util.getArg(s, 'offset');
  	    var offsetLine = util.getArg(offset, 'line');
  	    var offsetColumn = util.getArg(offset, 'column');

  	    if (offsetLine < lastOffset.line ||
  	        (offsetLine === lastOffset.line && offsetColumn < lastOffset.column)) {
  	      throw new Error('Section offsets must be ordered and non-overlapping.');
  	    }
  	    lastOffset = offset;

  	    return {
  	      generatedOffset: {
  	        // The offset fields are 0-based, but we use 1-based indices when
  	        // encoding/decoding from VLQ.
  	        generatedLine: offsetLine + 1,
  	        generatedColumn: offsetColumn + 1
  	      },
  	      consumer: new SourceMapConsumer(util.getArg(s, 'map'), aSourceMapURL)
  	    }
  	  });
  	}

  	IndexedSourceMapConsumer.prototype = Object.create(SourceMapConsumer.prototype);
  	IndexedSourceMapConsumer.prototype.constructor = SourceMapConsumer;

  	/**
  	 * The version of the source mapping spec that we are consuming.
  	 */
  	IndexedSourceMapConsumer.prototype._version = 3;

  	/**
  	 * The list of original sources.
  	 */
  	Object.defineProperty(IndexedSourceMapConsumer.prototype, 'sources', {
  	  get: function () {
  	    var sources = [];
  	    for (var i = 0; i < this._sections.length; i++) {
  	      for (var j = 0; j < this._sections[i].consumer.sources.length; j++) {
  	        sources.push(this._sections[i].consumer.sources[j]);
  	      }
  	    }
  	    return sources;
  	  }
  	});

  	/**
  	 * Returns the original source, line, and column information for the generated
  	 * source's line and column positions provided. The only argument is an object
  	 * with the following properties:
  	 *
  	 *   - line: The line number in the generated source.  The line number
  	 *     is 1-based.
  	 *   - column: The column number in the generated source.  The column
  	 *     number is 0-based.
  	 *
  	 * and an object is returned with the following properties:
  	 *
  	 *   - source: The original source file, or null.
  	 *   - line: The line number in the original source, or null.  The
  	 *     line number is 1-based.
  	 *   - column: The column number in the original source, or null.  The
  	 *     column number is 0-based.
  	 *   - name: The original identifier, or null.
  	 */
  	IndexedSourceMapConsumer.prototype.originalPositionFor =
  	  function IndexedSourceMapConsumer_originalPositionFor(aArgs) {
  	    var needle = {
  	      generatedLine: util.getArg(aArgs, 'line'),
  	      generatedColumn: util.getArg(aArgs, 'column')
  	    };

  	    // Find the section containing the generated position we're trying to map
  	    // to an original position.
  	    var sectionIndex = binarySearch.search(needle, this._sections,
  	      function(needle, section) {
  	        var cmp = needle.generatedLine - section.generatedOffset.generatedLine;
  	        if (cmp) {
  	          return cmp;
  	        }

  	        return (needle.generatedColumn -
  	                section.generatedOffset.generatedColumn);
  	      });
  	    var section = this._sections[sectionIndex];

  	    if (!section) {
  	      return {
  	        source: null,
  	        line: null,
  	        column: null,
  	        name: null
  	      };
  	    }

  	    return section.consumer.originalPositionFor({
  	      line: needle.generatedLine -
  	        (section.generatedOffset.generatedLine - 1),
  	      column: needle.generatedColumn -
  	        (section.generatedOffset.generatedLine === needle.generatedLine
  	         ? section.generatedOffset.generatedColumn - 1
  	         : 0),
  	      bias: aArgs.bias
  	    });
  	  };

  	/**
  	 * Return true if we have the source content for every source in the source
  	 * map, false otherwise.
  	 */
  	IndexedSourceMapConsumer.prototype.hasContentsOfAllSources =
  	  function IndexedSourceMapConsumer_hasContentsOfAllSources() {
  	    return this._sections.every(function (s) {
  	      return s.consumer.hasContentsOfAllSources();
  	    });
  	  };

  	/**
  	 * Returns the original source content. The only argument is the url of the
  	 * original source file. Returns null if no original source content is
  	 * available.
  	 */
  	IndexedSourceMapConsumer.prototype.sourceContentFor =
  	  function IndexedSourceMapConsumer_sourceContentFor(aSource, nullOnMissing) {
  	    for (var i = 0; i < this._sections.length; i++) {
  	      var section = this._sections[i];

  	      var content = section.consumer.sourceContentFor(aSource, true);
  	      if (content) {
  	        return content;
  	      }
  	    }
  	    if (nullOnMissing) {
  	      return null;
  	    }
  	    else {
  	      throw new Error('"' + aSource + '" is not in the SourceMap.');
  	    }
  	  };

  	/**
  	 * Returns the generated line and column information for the original source,
  	 * line, and column positions provided. The only argument is an object with
  	 * the following properties:
  	 *
  	 *   - source: The filename of the original source.
  	 *   - line: The line number in the original source.  The line number
  	 *     is 1-based.
  	 *   - column: The column number in the original source.  The column
  	 *     number is 0-based.
  	 *
  	 * and an object is returned with the following properties:
  	 *
  	 *   - line: The line number in the generated source, or null.  The
  	 *     line number is 1-based. 
  	 *   - column: The column number in the generated source, or null.
  	 *     The column number is 0-based.
  	 */
  	IndexedSourceMapConsumer.prototype.generatedPositionFor =
  	  function IndexedSourceMapConsumer_generatedPositionFor(aArgs) {
  	    for (var i = 0; i < this._sections.length; i++) {
  	      var section = this._sections[i];

  	      // Only consider this section if the requested source is in the list of
  	      // sources of the consumer.
  	      if (section.consumer._findSourceIndex(util.getArg(aArgs, 'source')) === -1) {
  	        continue;
  	      }
  	      var generatedPosition = section.consumer.generatedPositionFor(aArgs);
  	      if (generatedPosition) {
  	        var ret = {
  	          line: generatedPosition.line +
  	            (section.generatedOffset.generatedLine - 1),
  	          column: generatedPosition.column +
  	            (section.generatedOffset.generatedLine === generatedPosition.line
  	             ? section.generatedOffset.generatedColumn - 1
  	             : 0)
  	        };
  	        return ret;
  	      }
  	    }

  	    return {
  	      line: null,
  	      column: null
  	    };
  	  };

  	/**
  	 * Parse the mappings in a string in to a data structure which we can easily
  	 * query (the ordered arrays in the `this.__generatedMappings` and
  	 * `this.__originalMappings` properties).
  	 */
  	IndexedSourceMapConsumer.prototype._parseMappings =
  	  function IndexedSourceMapConsumer_parseMappings(aStr, aSourceRoot) {
  	    this.__generatedMappings = [];
  	    this.__originalMappings = [];
  	    for (var i = 0; i < this._sections.length; i++) {
  	      var section = this._sections[i];
  	      var sectionMappings = section.consumer._generatedMappings;
  	      for (var j = 0; j < sectionMappings.length; j++) {
  	        var mapping = sectionMappings[j];

  	        var source = section.consumer._sources.at(mapping.source);
  	        source = util.computeSourceURL(section.consumer.sourceRoot, source, this._sourceMapURL);
  	        this._sources.add(source);
  	        source = this._sources.indexOf(source);

  	        var name = null;
  	        if (mapping.name) {
  	          name = section.consumer._names.at(mapping.name);
  	          this._names.add(name);
  	          name = this._names.indexOf(name);
  	        }

  	        // The mappings coming from the consumer for the section have
  	        // generated positions relative to the start of the section, so we
  	        // need to offset them to be relative to the start of the concatenated
  	        // generated file.
  	        var adjustedMapping = {
  	          source: source,
  	          generatedLine: mapping.generatedLine +
  	            (section.generatedOffset.generatedLine - 1),
  	          generatedColumn: mapping.generatedColumn +
  	            (section.generatedOffset.generatedLine === mapping.generatedLine
  	            ? section.generatedOffset.generatedColumn - 1
  	            : 0),
  	          originalLine: mapping.originalLine,
  	          originalColumn: mapping.originalColumn,
  	          name: name
  	        };

  	        this.__generatedMappings.push(adjustedMapping);
  	        if (typeof adjustedMapping.originalLine === 'number') {
  	          this.__originalMappings.push(adjustedMapping);
  	        }
  	      }
  	    }

  	    quickSort(this.__generatedMappings, util.compareByGeneratedPositionsDeflated);
  	    quickSort(this.__originalMappings, util.compareByOriginalPositions);
  	  };

  	sourceMapConsumer.IndexedSourceMapConsumer = IndexedSourceMapConsumer;
  	return sourceMapConsumer;
  }

  var sourceNode = {};

  /* -*- Mode: js; js-indent-level: 2; -*- */

  var hasRequiredSourceNode;

  function requireSourceNode () {
  	if (hasRequiredSourceNode) return sourceNode;
  	hasRequiredSourceNode = 1;
  	/*
  	 * Copyright 2011 Mozilla Foundation and contributors
  	 * Licensed under the New BSD license. See LICENSE or:
  	 * http://opensource.org/licenses/BSD-3-Clause
  	 */

  	var SourceMapGenerator = requireSourceMapGenerator().SourceMapGenerator;
  	var util = requireUtil();

  	// Matches a Windows-style `\r\n` newline or a `\n` newline used by all other
  	// operating systems these days (capturing the result).
  	var REGEX_NEWLINE = /(\r?\n)/;

  	// Newline character code for charCodeAt() comparisons
  	var NEWLINE_CODE = 10;

  	// Private symbol for identifying `SourceNode`s when multiple versions of
  	// the source-map library are loaded. This MUST NOT CHANGE across
  	// versions!
  	var isSourceNode = "$$$isSourceNode$$$";

  	/**
  	 * SourceNodes provide a way to abstract over interpolating/concatenating
  	 * snippets of generated JavaScript source code while maintaining the line and
  	 * column information associated with the original source code.
  	 *
  	 * @param aLine The original line number.
  	 * @param aColumn The original column number.
  	 * @param aSource The original source's filename.
  	 * @param aChunks Optional. An array of strings which are snippets of
  	 *        generated JS, or other SourceNodes.
  	 * @param aName The original identifier.
  	 */
  	function SourceNode(aLine, aColumn, aSource, aChunks, aName) {
  	  this.children = [];
  	  this.sourceContents = {};
  	  this.line = aLine == null ? null : aLine;
  	  this.column = aColumn == null ? null : aColumn;
  	  this.source = aSource == null ? null : aSource;
  	  this.name = aName == null ? null : aName;
  	  this[isSourceNode] = true;
  	  if (aChunks != null) this.add(aChunks);
  	}

  	/**
  	 * Creates a SourceNode from generated code and a SourceMapConsumer.
  	 *
  	 * @param aGeneratedCode The generated code
  	 * @param aSourceMapConsumer The SourceMap for the generated code
  	 * @param aRelativePath Optional. The path that relative sources in the
  	 *        SourceMapConsumer should be relative to.
  	 */
  	SourceNode.fromStringWithSourceMap =
  	  function SourceNode_fromStringWithSourceMap(aGeneratedCode, aSourceMapConsumer, aRelativePath) {
  	    // The SourceNode we want to fill with the generated code
  	    // and the SourceMap
  	    var node = new SourceNode();

  	    // All even indices of this array are one line of the generated code,
  	    // while all odd indices are the newlines between two adjacent lines
  	    // (since `REGEX_NEWLINE` captures its match).
  	    // Processed fragments are accessed by calling `shiftNextLine`.
  	    var remainingLines = aGeneratedCode.split(REGEX_NEWLINE);
  	    var remainingLinesIndex = 0;
  	    var shiftNextLine = function() {
  	      var lineContents = getNextLine();
  	      // The last line of a file might not have a newline.
  	      var newLine = getNextLine() || "";
  	      return lineContents + newLine;

  	      function getNextLine() {
  	        return remainingLinesIndex < remainingLines.length ?
  	            remainingLines[remainingLinesIndex++] : undefined;
  	      }
  	    };

  	    // We need to remember the position of "remainingLines"
  	    var lastGeneratedLine = 1, lastGeneratedColumn = 0;

  	    // The generate SourceNodes we need a code range.
  	    // To extract it current and last mapping is used.
  	    // Here we store the last mapping.
  	    var lastMapping = null;

  	    aSourceMapConsumer.eachMapping(function (mapping) {
  	      if (lastMapping !== null) {
  	        // We add the code from "lastMapping" to "mapping":
  	        // First check if there is a new line in between.
  	        if (lastGeneratedLine < mapping.generatedLine) {
  	          // Associate first line with "lastMapping"
  	          addMappingWithCode(lastMapping, shiftNextLine());
  	          lastGeneratedLine++;
  	          lastGeneratedColumn = 0;
  	          // The remaining code is added without mapping
  	        } else {
  	          // There is no new line in between.
  	          // Associate the code between "lastGeneratedColumn" and
  	          // "mapping.generatedColumn" with "lastMapping"
  	          var nextLine = remainingLines[remainingLinesIndex] || '';
  	          var code = nextLine.substr(0, mapping.generatedColumn -
  	                                        lastGeneratedColumn);
  	          remainingLines[remainingLinesIndex] = nextLine.substr(mapping.generatedColumn -
  	                                              lastGeneratedColumn);
  	          lastGeneratedColumn = mapping.generatedColumn;
  	          addMappingWithCode(lastMapping, code);
  	          // No more remaining code, continue
  	          lastMapping = mapping;
  	          return;
  	        }
  	      }
  	      // We add the generated code until the first mapping
  	      // to the SourceNode without any mapping.
  	      // Each line is added as separate string.
  	      while (lastGeneratedLine < mapping.generatedLine) {
  	        node.add(shiftNextLine());
  	        lastGeneratedLine++;
  	      }
  	      if (lastGeneratedColumn < mapping.generatedColumn) {
  	        var nextLine = remainingLines[remainingLinesIndex] || '';
  	        node.add(nextLine.substr(0, mapping.generatedColumn));
  	        remainingLines[remainingLinesIndex] = nextLine.substr(mapping.generatedColumn);
  	        lastGeneratedColumn = mapping.generatedColumn;
  	      }
  	      lastMapping = mapping;
  	    }, this);
  	    // We have processed all mappings.
  	    if (remainingLinesIndex < remainingLines.length) {
  	      if (lastMapping) {
  	        // Associate the remaining code in the current line with "lastMapping"
  	        addMappingWithCode(lastMapping, shiftNextLine());
  	      }
  	      // and add the remaining lines without any mapping
  	      node.add(remainingLines.splice(remainingLinesIndex).join(""));
  	    }

  	    // Copy sourcesContent into SourceNode
  	    aSourceMapConsumer.sources.forEach(function (sourceFile) {
  	      var content = aSourceMapConsumer.sourceContentFor(sourceFile);
  	      if (content != null) {
  	        if (aRelativePath != null) {
  	          sourceFile = util.join(aRelativePath, sourceFile);
  	        }
  	        node.setSourceContent(sourceFile, content);
  	      }
  	    });

  	    return node;

  	    function addMappingWithCode(mapping, code) {
  	      if (mapping === null || mapping.source === undefined) {
  	        node.add(code);
  	      } else {
  	        var source = aRelativePath
  	          ? util.join(aRelativePath, mapping.source)
  	          : mapping.source;
  	        node.add(new SourceNode(mapping.originalLine,
  	                                mapping.originalColumn,
  	                                source,
  	                                code,
  	                                mapping.name));
  	      }
  	    }
  	  };

  	/**
  	 * Add a chunk of generated JS to this source node.
  	 *
  	 * @param aChunk A string snippet of generated JS code, another instance of
  	 *        SourceNode, or an array where each member is one of those things.
  	 */
  	SourceNode.prototype.add = function SourceNode_add(aChunk) {
  	  if (Array.isArray(aChunk)) {
  	    aChunk.forEach(function (chunk) {
  	      this.add(chunk);
  	    }, this);
  	  }
  	  else if (aChunk[isSourceNode] || typeof aChunk === "string") {
  	    if (aChunk) {
  	      this.children.push(aChunk);
  	    }
  	  }
  	  else {
  	    throw new TypeError(
  	      "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
  	    );
  	  }
  	  return this;
  	};

  	/**
  	 * Add a chunk of generated JS to the beginning of this source node.
  	 *
  	 * @param aChunk A string snippet of generated JS code, another instance of
  	 *        SourceNode, or an array where each member is one of those things.
  	 */
  	SourceNode.prototype.prepend = function SourceNode_prepend(aChunk) {
  	  if (Array.isArray(aChunk)) {
  	    for (var i = aChunk.length-1; i >= 0; i--) {
  	      this.prepend(aChunk[i]);
  	    }
  	  }
  	  else if (aChunk[isSourceNode] || typeof aChunk === "string") {
  	    this.children.unshift(aChunk);
  	  }
  	  else {
  	    throw new TypeError(
  	      "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
  	    );
  	  }
  	  return this;
  	};

  	/**
  	 * Walk over the tree of JS snippets in this node and its children. The
  	 * walking function is called once for each snippet of JS and is passed that
  	 * snippet and the its original associated source's line/column location.
  	 *
  	 * @param aFn The traversal function.
  	 */
  	SourceNode.prototype.walk = function SourceNode_walk(aFn) {
  	  var chunk;
  	  for (var i = 0, len = this.children.length; i < len; i++) {
  	    chunk = this.children[i];
  	    if (chunk[isSourceNode]) {
  	      chunk.walk(aFn);
  	    }
  	    else {
  	      if (chunk !== '') {
  	        aFn(chunk, { source: this.source,
  	                     line: this.line,
  	                     column: this.column,
  	                     name: this.name });
  	      }
  	    }
  	  }
  	};

  	/**
  	 * Like `String.prototype.join` except for SourceNodes. Inserts `aStr` between
  	 * each of `this.children`.
  	 *
  	 * @param aSep The separator.
  	 */
  	SourceNode.prototype.join = function SourceNode_join(aSep) {
  	  var newChildren;
  	  var i;
  	  var len = this.children.length;
  	  if (len > 0) {
  	    newChildren = [];
  	    for (i = 0; i < len-1; i++) {
  	      newChildren.push(this.children[i]);
  	      newChildren.push(aSep);
  	    }
  	    newChildren.push(this.children[i]);
  	    this.children = newChildren;
  	  }
  	  return this;
  	};

  	/**
  	 * Call String.prototype.replace on the very right-most source snippet. Useful
  	 * for trimming whitespace from the end of a source node, etc.
  	 *
  	 * @param aPattern The pattern to replace.
  	 * @param aReplacement The thing to replace the pattern with.
  	 */
  	SourceNode.prototype.replaceRight = function SourceNode_replaceRight(aPattern, aReplacement) {
  	  var lastChild = this.children[this.children.length - 1];
  	  if (lastChild[isSourceNode]) {
  	    lastChild.replaceRight(aPattern, aReplacement);
  	  }
  	  else if (typeof lastChild === 'string') {
  	    this.children[this.children.length - 1] = lastChild.replace(aPattern, aReplacement);
  	  }
  	  else {
  	    this.children.push(''.replace(aPattern, aReplacement));
  	  }
  	  return this;
  	};

  	/**
  	 * Set the source content for a source file. This will be added to the SourceMapGenerator
  	 * in the sourcesContent field.
  	 *
  	 * @param aSourceFile The filename of the source file
  	 * @param aSourceContent The content of the source file
  	 */
  	SourceNode.prototype.setSourceContent =
  	  function SourceNode_setSourceContent(aSourceFile, aSourceContent) {
  	    this.sourceContents[util.toSetString(aSourceFile)] = aSourceContent;
  	  };

  	/**
  	 * Walk over the tree of SourceNodes. The walking function is called for each
  	 * source file content and is passed the filename and source content.
  	 *
  	 * @param aFn The traversal function.
  	 */
  	SourceNode.prototype.walkSourceContents =
  	  function SourceNode_walkSourceContents(aFn) {
  	    for (var i = 0, len = this.children.length; i < len; i++) {
  	      if (this.children[i][isSourceNode]) {
  	        this.children[i].walkSourceContents(aFn);
  	      }
  	    }

  	    var sources = Object.keys(this.sourceContents);
  	    for (var i = 0, len = sources.length; i < len; i++) {
  	      aFn(util.fromSetString(sources[i]), this.sourceContents[sources[i]]);
  	    }
  	  };

  	/**
  	 * Return the string representation of this source node. Walks over the tree
  	 * and concatenates all the various snippets together to one string.
  	 */
  	SourceNode.prototype.toString = function SourceNode_toString() {
  	  var str = "";
  	  this.walk(function (chunk) {
  	    str += chunk;
  	  });
  	  return str;
  	};

  	/**
  	 * Returns the string representation of this source node along with a source
  	 * map.
  	 */
  	SourceNode.prototype.toStringWithSourceMap = function SourceNode_toStringWithSourceMap(aArgs) {
  	  var generated = {
  	    code: "",
  	    line: 1,
  	    column: 0
  	  };
  	  var map = new SourceMapGenerator(aArgs);
  	  var sourceMappingActive = false;
  	  var lastOriginalSource = null;
  	  var lastOriginalLine = null;
  	  var lastOriginalColumn = null;
  	  var lastOriginalName = null;
  	  this.walk(function (chunk, original) {
  	    generated.code += chunk;
  	    if (original.source !== null
  	        && original.line !== null
  	        && original.column !== null) {
  	      if(lastOriginalSource !== original.source
  	         || lastOriginalLine !== original.line
  	         || lastOriginalColumn !== original.column
  	         || lastOriginalName !== original.name) {
  	        map.addMapping({
  	          source: original.source,
  	          original: {
  	            line: original.line,
  	            column: original.column
  	          },
  	          generated: {
  	            line: generated.line,
  	            column: generated.column
  	          },
  	          name: original.name
  	        });
  	      }
  	      lastOriginalSource = original.source;
  	      lastOriginalLine = original.line;
  	      lastOriginalColumn = original.column;
  	      lastOriginalName = original.name;
  	      sourceMappingActive = true;
  	    } else if (sourceMappingActive) {
  	      map.addMapping({
  	        generated: {
  	          line: generated.line,
  	          column: generated.column
  	        }
  	      });
  	      lastOriginalSource = null;
  	      sourceMappingActive = false;
  	    }
  	    for (var idx = 0, length = chunk.length; idx < length; idx++) {
  	      if (chunk.charCodeAt(idx) === NEWLINE_CODE) {
  	        generated.line++;
  	        generated.column = 0;
  	        // Mappings end at eol
  	        if (idx + 1 === length) {
  	          lastOriginalSource = null;
  	          sourceMappingActive = false;
  	        } else if (sourceMappingActive) {
  	          map.addMapping({
  	            source: original.source,
  	            original: {
  	              line: original.line,
  	              column: original.column
  	            },
  	            generated: {
  	              line: generated.line,
  	              column: generated.column
  	            },
  	            name: original.name
  	          });
  	        }
  	      } else {
  	        generated.column++;
  	      }
  	    }
  	  });
  	  this.walkSourceContents(function (sourceFile, sourceContent) {
  	    map.setSourceContent(sourceFile, sourceContent);
  	  });

  	  return { code: generated.code, map: map };
  	};

  	sourceNode.SourceNode = SourceNode;
  	return sourceNode;
  }

  /*
   * Copyright 2009-2011 Mozilla Foundation and contributors
   * Licensed under the New BSD license. See LICENSE.txt or:
   * http://opensource.org/licenses/BSD-3-Clause
   */

  var hasRequiredSourceMap;

  function requireSourceMap () {
  	if (hasRequiredSourceMap) return sourceMap;
  	hasRequiredSourceMap = 1;
  	sourceMap.SourceMapGenerator = requireSourceMapGenerator().SourceMapGenerator;
  	sourceMap.SourceMapConsumer = requireSourceMapConsumer().SourceMapConsumer;
  	sourceMap.SourceNode = requireSourceNode().SourceNode;
  	return sourceMap;
  }

  var version = "2.1.0";
  var require$$3 = {
  	version: version};

  /*
    Copyright (C) 2012-2014 Yusuke Suzuki <utatane.tea@gmail.com>
    Copyright (C) 2015 Ingvar Stepanyan <me@rreverser.com>
    Copyright (C) 2014 Ivan Nikulin <ifaaan@gmail.com>
    Copyright (C) 2012-2013 Michael Ficarra <escodegen.copyright@michael.ficarra.me>
    Copyright (C) 2012-2013 Mathias Bynens <mathias@qiwi.be>
    Copyright (C) 2013 Irakli Gozalishvili <rfobic@gmail.com>
    Copyright (C) 2012 Robert Gust-Bardon <donate@robert.gust-bardon.org>
    Copyright (C) 2012 John Freeman <jfreeman08@gmail.com>
    Copyright (C) 2011-2012 Ariya Hidayat <ariya.hidayat@gmail.com>
    Copyright (C) 2012 Joost-Wim Boekesteijn <joost-wim@boekesteijn.nl>
    Copyright (C) 2012 Kris Kowal <kris.kowal@cixar.com>
    Copyright (C) 2012 Arpad Borsos <arpad.borsos@googlemail.com>
    Copyright (C) 2020 Apple Inc. All rights reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:

      * Redistributions of source code must retain the above copyright
        notice, this list of conditions and the following disclaimer.
      * Redistributions in binary form must reproduce the above copyright
        notice, this list of conditions and the following disclaimer in the
        documentation and/or other materials provided with the distribution.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
    DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
    ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
    THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */

  (function (exports$1) {
  	/*global exports:true, require:true, global:true*/
  	(function () {

  	    var Syntax,
  	        Precedence,
  	        BinaryPrecedence,
  	        SourceNode,
  	        estraverse$1,
  	        esutils,
  	        base,
  	        indent,
  	        json,
  	        renumber,
  	        hexadecimal,
  	        quotes,
  	        escapeless,
  	        newline,
  	        space,
  	        parentheses,
  	        semicolons,
  	        safeConcatenation,
  	        directive,
  	        extra,
  	        parse,
  	        sourceMap,
  	        sourceCode,
  	        preserveBlankLines,
  	        FORMAT_MINIFY,
  	        FORMAT_DEFAULTS;

  	    estraverse$1 = estraverse;
  	    esutils = utils;

  	    Syntax = estraverse$1.Syntax;

  	    // Generation is done by generateExpression.
  	    function isExpression(node) {
  	        return CodeGenerator.Expression.hasOwnProperty(node.type);
  	    }

  	    // Generation is done by generateStatement.
  	    function isStatement(node) {
  	        return CodeGenerator.Statement.hasOwnProperty(node.type);
  	    }

  	    Precedence = {
  	        Sequence: 0,
  	        Yield: 1,
  	        Assignment: 1,
  	        Conditional: 2,
  	        ArrowFunction: 2,
  	        Coalesce: 3,
  	        LogicalOR: 4,
  	        LogicalAND: 5,
  	        BitwiseOR: 6,
  	        BitwiseXOR: 7,
  	        BitwiseAND: 8,
  	        Equality: 9,
  	        Relational: 10,
  	        BitwiseSHIFT: 11,
  	        Additive: 12,
  	        Multiplicative: 13,
  	        Exponentiation: 14,
  	        Await: 15,
  	        Unary: 15,
  	        Postfix: 16,
  	        OptionalChaining: 17,
  	        Call: 18,
  	        New: 19,
  	        TaggedTemplate: 20,
  	        Member: 21,
  	        Primary: 22
  	    };

  	    BinaryPrecedence = {
  	        '??': Precedence.Coalesce,
  	        '||': Precedence.LogicalOR,
  	        '&&': Precedence.LogicalAND,
  	        '|': Precedence.BitwiseOR,
  	        '^': Precedence.BitwiseXOR,
  	        '&': Precedence.BitwiseAND,
  	        '==': Precedence.Equality,
  	        '!=': Precedence.Equality,
  	        '===': Precedence.Equality,
  	        '!==': Precedence.Equality,
  	        'is': Precedence.Equality,
  	        'isnt': Precedence.Equality,
  	        '<': Precedence.Relational,
  	        '>': Precedence.Relational,
  	        '<=': Precedence.Relational,
  	        '>=': Precedence.Relational,
  	        'in': Precedence.Relational,
  	        'instanceof': Precedence.Relational,
  	        '<<': Precedence.BitwiseSHIFT,
  	        '>>': Precedence.BitwiseSHIFT,
  	        '>>>': Precedence.BitwiseSHIFT,
  	        '+': Precedence.Additive,
  	        '-': Precedence.Additive,
  	        '*': Precedence.Multiplicative,
  	        '%': Precedence.Multiplicative,
  	        '/': Precedence.Multiplicative,
  	        '**': Precedence.Exponentiation
  	    };

  	    //Flags
  	    var F_ALLOW_IN = 1,
  	        F_ALLOW_CALL = 1 << 1,
  	        F_ALLOW_UNPARATH_NEW = 1 << 2,
  	        F_FUNC_BODY = 1 << 3,
  	        F_DIRECTIVE_CTX = 1 << 4,
  	        F_SEMICOLON_OPT = 1 << 5,
  	        F_FOUND_COALESCE = 1 << 6;

  	    //Expression flag sets
  	    //NOTE: Flag order:
  	    // F_ALLOW_IN
  	    // F_ALLOW_CALL
  	    // F_ALLOW_UNPARATH_NEW
  	    var E_FTT = F_ALLOW_CALL | F_ALLOW_UNPARATH_NEW,
  	        E_TTF = F_ALLOW_IN | F_ALLOW_CALL,
  	        E_TTT = F_ALLOW_IN | F_ALLOW_CALL | F_ALLOW_UNPARATH_NEW,
  	        E_TFF = F_ALLOW_IN,
  	        E_FFT = F_ALLOW_UNPARATH_NEW,
  	        E_TFT = F_ALLOW_IN | F_ALLOW_UNPARATH_NEW;

  	    //Statement flag sets
  	    //NOTE: Flag order:
  	    // F_ALLOW_IN
  	    // F_FUNC_BODY
  	    // F_DIRECTIVE_CTX
  	    // F_SEMICOLON_OPT
  	    var S_TFFF = F_ALLOW_IN,
  	        S_TFFT = F_ALLOW_IN | F_SEMICOLON_OPT,
  	        S_FFFF = 0x00,
  	        S_TFTF = F_ALLOW_IN | F_DIRECTIVE_CTX,
  	        S_TTFF = F_ALLOW_IN | F_FUNC_BODY;

  	    function getDefaultOptions() {
  	        // default options
  	        return {
  	            indent: null,
  	            base: null,
  	            parse: null,
  	            comment: false,
  	            format: {
  	                indent: {
  	                    style: '    ',
  	                    base: 0,
  	                    adjustMultilineComment: false
  	                },
  	                newline: '\n',
  	                space: ' ',
  	                json: false,
  	                renumber: false,
  	                hexadecimal: false,
  	                quotes: 'single',
  	                escapeless: false,
  	                compact: false,
  	                parentheses: true,
  	                semicolons: true,
  	                safeConcatenation: false,
  	                preserveBlankLines: false
  	            },
  	            moz: {
  	                comprehensionExpressionStartsWithAssignment: false,
  	                starlessGenerator: false
  	            },
  	            sourceMap: null,
  	            sourceMapRoot: null,
  	            sourceMapWithCode: false,
  	            directive: false,
  	            raw: true,
  	            verbatim: null,
  	            sourceCode: null
  	        };
  	    }

  	    function stringRepeat(str, num) {
  	        var result = '';

  	        for (num |= 0; num > 0; num >>>= 1, str += str) {
  	            if (num & 1) {
  	                result += str;
  	            }
  	        }

  	        return result;
  	    }

  	    function hasLineTerminator(str) {
  	        return (/[\r\n]/g).test(str);
  	    }

  	    function endsWithLineTerminator(str) {
  	        var len = str.length;
  	        return len && esutils.code.isLineTerminator(str.charCodeAt(len - 1));
  	    }

  	    function merge(target, override) {
  	        var key;
  	        for (key in override) {
  	            if (override.hasOwnProperty(key)) {
  	                target[key] = override[key];
  	            }
  	        }
  	        return target;
  	    }

  	    function updateDeeply(target, override) {
  	        var key, val;

  	        function isHashObject(target) {
  	            return typeof target === 'object' && target instanceof Object && !(target instanceof RegExp);
  	        }

  	        for (key in override) {
  	            if (override.hasOwnProperty(key)) {
  	                val = override[key];
  	                if (isHashObject(val)) {
  	                    if (isHashObject(target[key])) {
  	                        updateDeeply(target[key], val);
  	                    } else {
  	                        target[key] = updateDeeply({}, val);
  	                    }
  	                } else {
  	                    target[key] = val;
  	                }
  	            }
  	        }
  	        return target;
  	    }

  	    function generateNumber(value) {
  	        var result, point, temp, exponent, pos;

  	        if (value !== value) {
  	            throw new Error('Numeric literal whose value is NaN');
  	        }
  	        if (value < 0 || (value === 0 && 1 / value < 0)) {
  	            throw new Error('Numeric literal whose value is negative');
  	        }

  	        if (value === 1 / 0) {
  	            return json ? 'null' : renumber ? '1e400' : '1e+400';
  	        }

  	        result = '' + value;
  	        if (!renumber || result.length < 3) {
  	            return result;
  	        }

  	        point = result.indexOf('.');
  	        if (!json && result.charCodeAt(0) === 0x30  /* 0 */ && point === 1) {
  	            point = 0;
  	            result = result.slice(1);
  	        }
  	        temp = result;
  	        result = result.replace('e+', 'e');
  	        exponent = 0;
  	        if ((pos = temp.indexOf('e')) > 0) {
  	            exponent = +temp.slice(pos + 1);
  	            temp = temp.slice(0, pos);
  	        }
  	        if (point >= 0) {
  	            exponent -= temp.length - point - 1;
  	            temp = +(temp.slice(0, point) + temp.slice(point + 1)) + '';
  	        }
  	        pos = 0;
  	        while (temp.charCodeAt(temp.length + pos - 1) === 0x30  /* 0 */) {
  	            --pos;
  	        }
  	        if (pos !== 0) {
  	            exponent -= pos;
  	            temp = temp.slice(0, pos);
  	        }
  	        if (exponent !== 0) {
  	            temp += 'e' + exponent;
  	        }
  	        if ((temp.length < result.length ||
  	                    (hexadecimal && value > 1e12 && Math.floor(value) === value && (temp = '0x' + value.toString(16)).length < result.length)) &&
  	                +temp === value) {
  	            result = temp;
  	        }

  	        return result;
  	    }

  	    // Generate valid RegExp expression.
  	    // This function is based on https://github.com/Constellation/iv Engine

  	    function escapeRegExpCharacter(ch, previousIsBackslash) {
  	        // not handling '\' and handling \u2028 or \u2029 to unicode escape sequence
  	        if ((ch & -2) === 0x2028) {
  	            return (previousIsBackslash ? 'u' : '\\u') + ((ch === 0x2028) ? '2028' : '2029');
  	        } else if (ch === 10 || ch === 13) {  // \n, \r
  	            return (previousIsBackslash ? '' : '\\') + ((ch === 10) ? 'n' : 'r');
  	        }
  	        return String.fromCharCode(ch);
  	    }

  	    function generateRegExp(reg) {
  	        var match, result, flags, i, iz, ch, characterInBrack, previousIsBackslash;

  	        result = reg.toString();

  	        if (reg.source) {
  	            // extract flag from toString result
  	            match = result.match(/\/([^/]*)$/);
  	            if (!match) {
  	                return result;
  	            }

  	            flags = match[1];
  	            result = '';

  	            characterInBrack = false;
  	            previousIsBackslash = false;
  	            for (i = 0, iz = reg.source.length; i < iz; ++i) {
  	                ch = reg.source.charCodeAt(i);

  	                if (!previousIsBackslash) {
  	                    if (characterInBrack) {
  	                        if (ch === 93) {  // ]
  	                            characterInBrack = false;
  	                        }
  	                    } else {
  	                        if (ch === 47) {  // /
  	                            result += '\\';
  	                        } else if (ch === 91) {  // [
  	                            characterInBrack = true;
  	                        }
  	                    }
  	                    result += escapeRegExpCharacter(ch, previousIsBackslash);
  	                    previousIsBackslash = ch === 92;  // \
  	                } else {
  	                    // if new RegExp("\\\n') is provided, create /\n/
  	                    result += escapeRegExpCharacter(ch, previousIsBackslash);
  	                    // prevent like /\\[/]/
  	                    previousIsBackslash = false;
  	                }
  	            }

  	            return '/' + result + '/' + flags;
  	        }

  	        return result;
  	    }

  	    function escapeAllowedCharacter(code, next) {
  	        var hex;

  	        if (code === 0x08  /* \b */) {
  	            return '\\b';
  	        }

  	        if (code === 0x0C  /* \f */) {
  	            return '\\f';
  	        }

  	        if (code === 0x09  /* \t */) {
  	            return '\\t';
  	        }

  	        hex = code.toString(16).toUpperCase();
  	        if (json || code > 0xFF) {
  	            return '\\u' + '0000'.slice(hex.length) + hex;
  	        } else if (code === 0x0000 && !esutils.code.isDecimalDigit(next)) {
  	            return '\\0';
  	        } else if (code === 0x000B  /* \v */) { // '\v'
  	            return '\\x0B';
  	        } else {
  	            return '\\x' + '00'.slice(hex.length) + hex;
  	        }
  	    }

  	    function escapeDisallowedCharacter(code) {
  	        if (code === 0x5C  /* \ */) {
  	            return '\\\\';
  	        }

  	        if (code === 0x0A  /* \n */) {
  	            return '\\n';
  	        }

  	        if (code === 0x0D  /* \r */) {
  	            return '\\r';
  	        }

  	        if (code === 0x2028) {
  	            return '\\u2028';
  	        }

  	        if (code === 0x2029) {
  	            return '\\u2029';
  	        }

  	        throw new Error('Incorrectly classified character');
  	    }

  	    function escapeDirective(str) {
  	        var i, iz, code, quote;

  	        quote = quotes === 'double' ? '"' : '\'';
  	        for (i = 0, iz = str.length; i < iz; ++i) {
  	            code = str.charCodeAt(i);
  	            if (code === 0x27  /* ' */) {
  	                quote = '"';
  	                break;
  	            } else if (code === 0x22  /* " */) {
  	                quote = '\'';
  	                break;
  	            } else if (code === 0x5C  /* \ */) {
  	                ++i;
  	            }
  	        }

  	        return quote + str + quote;
  	    }

  	    function escapeString(str) {
  	        var result = '', i, len, code, singleQuotes = 0, doubleQuotes = 0, single, quote;

  	        for (i = 0, len = str.length; i < len; ++i) {
  	            code = str.charCodeAt(i);
  	            if (code === 0x27  /* ' */) {
  	                ++singleQuotes;
  	            } else if (code === 0x22  /* " */) {
  	                ++doubleQuotes;
  	            } else if (code === 0x2F  /* / */ && json) {
  	                result += '\\';
  	            } else if (esutils.code.isLineTerminator(code) || code === 0x5C  /* \ */) {
  	                result += escapeDisallowedCharacter(code);
  	                continue;
  	            } else if (!esutils.code.isIdentifierPartES5(code) && (json && code < 0x20  /* SP */ || !json && !escapeless && (code < 0x20  /* SP */ || code > 0x7E  /* ~ */))) {
  	                result += escapeAllowedCharacter(code, str.charCodeAt(i + 1));
  	                continue;
  	            }
  	            result += String.fromCharCode(code);
  	        }

  	        single = !(quotes === 'double' || (quotes === 'auto' && doubleQuotes < singleQuotes));
  	        quote = single ? '\'' : '"';

  	        if (!(single ? singleQuotes : doubleQuotes)) {
  	            return quote + result + quote;
  	        }

  	        str = result;
  	        result = quote;

  	        for (i = 0, len = str.length; i < len; ++i) {
  	            code = str.charCodeAt(i);
  	            if ((code === 0x27  /* ' */ && single) || (code === 0x22  /* " */ && !single)) {
  	                result += '\\';
  	            }
  	            result += String.fromCharCode(code);
  	        }

  	        return result + quote;
  	    }

  	    /**
  	     * flatten an array to a string, where the array can contain
  	     * either strings or nested arrays
  	     */
  	    function flattenToString(arr) {
  	        var i, iz, elem, result = '';
  	        for (i = 0, iz = arr.length; i < iz; ++i) {
  	            elem = arr[i];
  	            result += Array.isArray(elem) ? flattenToString(elem) : elem;
  	        }
  	        return result;
  	    }

  	    /**
  	     * convert generated to a SourceNode when source maps are enabled.
  	     */
  	    function toSourceNodeWhenNeeded(generated, node) {
  	        if (!sourceMap) {
  	            // with no source maps, generated is either an
  	            // array or a string.  if an array, flatten it.
  	            // if a string, just return it
  	            if (Array.isArray(generated)) {
  	                return flattenToString(generated);
  	            } else {
  	                return generated;
  	            }
  	        }
  	        if (node == null) {
  	            if (generated instanceof SourceNode) {
  	                return generated;
  	            } else {
  	                node = {};
  	            }
  	        }
  	        if (node.loc == null) {
  	            return new SourceNode(null, null, sourceMap, generated, node.name || null);
  	        }
  	        return new SourceNode(node.loc.start.line, node.loc.start.column, (sourceMap === true ? node.loc.source || null : sourceMap), generated, node.name || null);
  	    }

  	    function noEmptySpace() {
  	        return (space) ? space : ' ';
  	    }

  	    function join(left, right) {
  	        var leftSource,
  	            rightSource,
  	            leftCharCode,
  	            rightCharCode;

  	        leftSource = toSourceNodeWhenNeeded(left).toString();
  	        if (leftSource.length === 0) {
  	            return [right];
  	        }

  	        rightSource = toSourceNodeWhenNeeded(right).toString();
  	        if (rightSource.length === 0) {
  	            return [left];
  	        }

  	        leftCharCode = leftSource.charCodeAt(leftSource.length - 1);
  	        rightCharCode = rightSource.charCodeAt(0);

  	        if ((leftCharCode === 0x2B  /* + */ || leftCharCode === 0x2D  /* - */) && leftCharCode === rightCharCode ||
  	            esutils.code.isIdentifierPartES5(leftCharCode) && esutils.code.isIdentifierPartES5(rightCharCode) ||
  	            leftCharCode === 0x2F  /* / */ && rightCharCode === 0x69  /* i */) { // infix word operators all start with `i`
  	            return [left, noEmptySpace(), right];
  	        } else if (esutils.code.isWhiteSpace(leftCharCode) || esutils.code.isLineTerminator(leftCharCode) ||
  	                esutils.code.isWhiteSpace(rightCharCode) || esutils.code.isLineTerminator(rightCharCode)) {
  	            return [left, right];
  	        }
  	        return [left, space, right];
  	    }

  	    function addIndent(stmt) {
  	        return [base, stmt];
  	    }

  	    function withIndent(fn) {
  	        var previousBase;
  	        previousBase = base;
  	        base += indent;
  	        fn(base);
  	        base = previousBase;
  	    }

  	    function calculateSpaces(str) {
  	        var i;
  	        for (i = str.length - 1; i >= 0; --i) {
  	            if (esutils.code.isLineTerminator(str.charCodeAt(i))) {
  	                break;
  	            }
  	        }
  	        return (str.length - 1) - i;
  	    }

  	    function adjustMultilineComment(value, specialBase) {
  	        var array, i, len, line, j, spaces, previousBase, sn;

  	        array = value.split(/\r\n|[\r\n]/);
  	        spaces = Number.MAX_VALUE;

  	        // first line doesn't have indentation
  	        for (i = 1, len = array.length; i < len; ++i) {
  	            line = array[i];
  	            j = 0;
  	            while (j < line.length && esutils.code.isWhiteSpace(line.charCodeAt(j))) {
  	                ++j;
  	            }
  	            if (spaces > j) {
  	                spaces = j;
  	            }
  	        }

  	        if (typeof specialBase !== 'undefined') {
  	            // pattern like
  	            // {
  	            //   var t = 20;  /*
  	            //                 * this is comment
  	            //                 */
  	            // }
  	            previousBase = base;
  	            if (array[1][spaces] === '*') {
  	                specialBase += ' ';
  	            }
  	            base = specialBase;
  	        } else {
  	            if (spaces & 1) {
  	                // /*
  	                //  *
  	                //  */
  	                // If spaces are odd number, above pattern is considered.
  	                // We waste 1 space.
  	                --spaces;
  	            }
  	            previousBase = base;
  	        }

  	        for (i = 1, len = array.length; i < len; ++i) {
  	            sn = toSourceNodeWhenNeeded(addIndent(array[i].slice(spaces)));
  	            array[i] = sourceMap ? sn.join('') : sn;
  	        }

  	        base = previousBase;

  	        return array.join('\n');
  	    }

  	    function generateComment(comment, specialBase) {
  	        if (comment.type === 'Line') {
  	            if (endsWithLineTerminator(comment.value)) {
  	                return '//' + comment.value;
  	            } else {
  	                // Always use LineTerminator
  	                var result = '//' + comment.value;
  	                if (!preserveBlankLines) {
  	                    result += '\n';
  	                }
  	                return result;
  	            }
  	        }
  	        if (extra.format.indent.adjustMultilineComment && /[\n\r]/.test(comment.value)) {
  	            return adjustMultilineComment('/*' + comment.value + '*/', specialBase);
  	        }
  	        return '/*' + comment.value + '*/';
  	    }

  	    function addComments(stmt, result) {
  	        var i, len, comment, save, tailingToStatement, specialBase, fragment,
  	            extRange, range, prevRange, prefix, infix, suffix, count;

  	        if (stmt.leadingComments && stmt.leadingComments.length > 0) {
  	            save = result;

  	            if (preserveBlankLines) {
  	                comment = stmt.leadingComments[0];
  	                result = [];

  	                extRange = comment.extendedRange;
  	                range = comment.range;

  	                prefix = sourceCode.substring(extRange[0], range[0]);
  	                count = (prefix.match(/\n/g) || []).length;
  	                if (count > 0) {
  	                    result.push(stringRepeat('\n', count));
  	                    result.push(addIndent(generateComment(comment)));
  	                } else {
  	                    result.push(prefix);
  	                    result.push(generateComment(comment));
  	                }

  	                prevRange = range;

  	                for (i = 1, len = stmt.leadingComments.length; i < len; i++) {
  	                    comment = stmt.leadingComments[i];
  	                    range = comment.range;

  	                    infix = sourceCode.substring(prevRange[1], range[0]);
  	                    count = (infix.match(/\n/g) || []).length;
  	                    result.push(stringRepeat('\n', count));
  	                    result.push(addIndent(generateComment(comment)));

  	                    prevRange = range;
  	                }

  	                suffix = sourceCode.substring(range[1], extRange[1]);
  	                count = (suffix.match(/\n/g) || []).length;
  	                result.push(stringRepeat('\n', count));
  	            } else {
  	                comment = stmt.leadingComments[0];
  	                result = [];
  	                if (safeConcatenation && stmt.type === Syntax.Program && stmt.body.length === 0) {
  	                    result.push('\n');
  	                }
  	                result.push(generateComment(comment));
  	                if (!endsWithLineTerminator(toSourceNodeWhenNeeded(result).toString())) {
  	                    result.push('\n');
  	                }

  	                for (i = 1, len = stmt.leadingComments.length; i < len; ++i) {
  	                    comment = stmt.leadingComments[i];
  	                    fragment = [generateComment(comment)];
  	                    if (!endsWithLineTerminator(toSourceNodeWhenNeeded(fragment).toString())) {
  	                        fragment.push('\n');
  	                    }
  	                    result.push(addIndent(fragment));
  	                }
  	            }

  	            result.push(addIndent(save));
  	        }

  	        if (stmt.trailingComments) {

  	            if (preserveBlankLines) {
  	                comment = stmt.trailingComments[0];
  	                extRange = comment.extendedRange;
  	                range = comment.range;

  	                prefix = sourceCode.substring(extRange[0], range[0]);
  	                count = (prefix.match(/\n/g) || []).length;

  	                if (count > 0) {
  	                    result.push(stringRepeat('\n', count));
  	                    result.push(addIndent(generateComment(comment)));
  	                } else {
  	                    result.push(prefix);
  	                    result.push(generateComment(comment));
  	                }
  	            } else {
  	                tailingToStatement = !endsWithLineTerminator(toSourceNodeWhenNeeded(result).toString());
  	                specialBase = stringRepeat(' ', calculateSpaces(toSourceNodeWhenNeeded([base, result, indent]).toString()));
  	                for (i = 0, len = stmt.trailingComments.length; i < len; ++i) {
  	                    comment = stmt.trailingComments[i];
  	                    if (tailingToStatement) {
  	                        // We assume target like following script
  	                        //
  	                        // var t = 20;  /**
  	                        //               * This is comment of t
  	                        //               */
  	                        if (i === 0) {
  	                            // first case
  	                            result = [result, indent];
  	                        } else {
  	                            result = [result, specialBase];
  	                        }
  	                        result.push(generateComment(comment, specialBase));
  	                    } else {
  	                        result = [result, addIndent(generateComment(comment))];
  	                    }
  	                    if (i !== len - 1 && !endsWithLineTerminator(toSourceNodeWhenNeeded(result).toString())) {
  	                        result = [result, '\n'];
  	                    }
  	                }
  	            }
  	        }

  	        return result;
  	    }

  	    function generateBlankLines(start, end, result) {
  	        var j, newlineCount = 0;

  	        for (j = start; j < end; j++) {
  	            if (sourceCode[j] === '\n') {
  	                newlineCount++;
  	            }
  	        }

  	        for (j = 1; j < newlineCount; j++) {
  	            result.push(newline);
  	        }
  	    }

  	    function parenthesize(text, current, should) {
  	        if (current < should) {
  	            return ['(', text, ')'];
  	        }
  	        return text;
  	    }

  	    function generateVerbatimString(string) {
  	        var i, iz, result;
  	        result = string.split(/\r\n|\n/);
  	        for (i = 1, iz = result.length; i < iz; i++) {
  	            result[i] = newline + base + result[i];
  	        }
  	        return result;
  	    }

  	    function generateVerbatim(expr, precedence) {
  	        var verbatim, result, prec;
  	        verbatim = expr[extra.verbatim];

  	        if (typeof verbatim === 'string') {
  	            result = parenthesize(generateVerbatimString(verbatim), Precedence.Sequence, precedence);
  	        } else {
  	            // verbatim is object
  	            result = generateVerbatimString(verbatim.content);
  	            prec = (verbatim.precedence != null) ? verbatim.precedence : Precedence.Sequence;
  	            result = parenthesize(result, prec, precedence);
  	        }

  	        return toSourceNodeWhenNeeded(result, expr);
  	    }

  	    function CodeGenerator() {
  	    }

  	    // Helpers.

  	    CodeGenerator.prototype.maybeBlock = function(stmt, flags) {
  	        var result, noLeadingComment, that = this;

  	        noLeadingComment = !extra.comment || !stmt.leadingComments;

  	        if (stmt.type === Syntax.BlockStatement && noLeadingComment) {
  	            return [space, this.generateStatement(stmt, flags)];
  	        }

  	        if (stmt.type === Syntax.EmptyStatement && noLeadingComment) {
  	            return ';';
  	        }

  	        withIndent(function () {
  	            result = [
  	                newline,
  	                addIndent(that.generateStatement(stmt, flags))
  	            ];
  	        });

  	        return result;
  	    };

  	    CodeGenerator.prototype.maybeBlockSuffix = function (stmt, result) {
  	        var ends = endsWithLineTerminator(toSourceNodeWhenNeeded(result).toString());
  	        if (stmt.type === Syntax.BlockStatement && (!extra.comment || !stmt.leadingComments) && !ends) {
  	            return [result, space];
  	        }
  	        if (ends) {
  	            return [result, base];
  	        }
  	        return [result, newline, base];
  	    };

  	    function generateIdentifier(node) {
  	        return toSourceNodeWhenNeeded(node.name, node);
  	    }

  	    function generateAsyncPrefix(node, spaceRequired) {
  	        return node.async ? 'async' + (spaceRequired ? noEmptySpace() : space) : '';
  	    }

  	    function generateStarSuffix(node) {
  	        var isGenerator = node.generator && !extra.moz.starlessGenerator;
  	        return isGenerator ? '*' + space : '';
  	    }

  	    function generateMethodPrefix(prop) {
  	        var func = prop.value, prefix = '';
  	        if (func.async) {
  	            prefix += generateAsyncPrefix(func, !prop.computed);
  	        }
  	        if (func.generator) {
  	            // avoid space before method name
  	            prefix += generateStarSuffix(func) ? '*' : '';
  	        }
  	        return prefix;
  	    }

  	    CodeGenerator.prototype.generatePattern = function (node, precedence, flags) {
  	        if (node.type === Syntax.Identifier) {
  	            return generateIdentifier(node);
  	        }
  	        return this.generateExpression(node, precedence, flags);
  	    };

  	    CodeGenerator.prototype.generateFunctionParams = function (node) {
  	        var i, iz, result, hasDefault;

  	        hasDefault = false;

  	        if (node.type === Syntax.ArrowFunctionExpression &&
  	                !node.rest && (!node.defaults || node.defaults.length === 0) &&
  	                node.params.length === 1 && node.params[0].type === Syntax.Identifier) {
  	            // arg => { } case
  	            result = [generateAsyncPrefix(node, true), generateIdentifier(node.params[0])];
  	        } else {
  	            result = node.type === Syntax.ArrowFunctionExpression ? [generateAsyncPrefix(node, false)] : [];
  	            result.push('(');
  	            if (node.defaults) {
  	                hasDefault = true;
  	            }
  	            for (i = 0, iz = node.params.length; i < iz; ++i) {
  	                if (hasDefault && node.defaults[i]) {
  	                    // Handle default values.
  	                    result.push(this.generateAssignment(node.params[i], node.defaults[i], '=', Precedence.Assignment, E_TTT));
  	                } else {
  	                    result.push(this.generatePattern(node.params[i], Precedence.Assignment, E_TTT));
  	                }
  	                if (i + 1 < iz) {
  	                    result.push(',' + space);
  	                }
  	            }

  	            if (node.rest) {
  	                if (node.params.length) {
  	                    result.push(',' + space);
  	                }
  	                result.push('...');
  	                result.push(generateIdentifier(node.rest));
  	            }

  	            result.push(')');
  	        }

  	        return result;
  	    };

  	    CodeGenerator.prototype.generateFunctionBody = function (node) {
  	        var result, expr;

  	        result = this.generateFunctionParams(node);

  	        if (node.type === Syntax.ArrowFunctionExpression) {
  	            result.push(space);
  	            result.push('=>');
  	        }

  	        if (node.expression) {
  	            result.push(space);
  	            expr = this.generateExpression(node.body, Precedence.Assignment, E_TTT);
  	            if (expr.toString().charAt(0) === '{') {
  	                expr = ['(', expr, ')'];
  	            }
  	            result.push(expr);
  	        } else {
  	            result.push(this.maybeBlock(node.body, S_TTFF));
  	        }

  	        return result;
  	    };

  	    CodeGenerator.prototype.generateIterationForStatement = function (operator, stmt, flags) {
  	        var result = ['for' + (stmt.await ? noEmptySpace() + 'await' : '') + space + '('], that = this;
  	        withIndent(function () {
  	            if (stmt.left.type === Syntax.VariableDeclaration) {
  	                withIndent(function () {
  	                    result.push(stmt.left.kind + noEmptySpace());
  	                    result.push(that.generateStatement(stmt.left.declarations[0], S_FFFF));
  	                });
  	            } else {
  	                result.push(that.generateExpression(stmt.left, Precedence.Call, E_TTT));
  	            }

  	            result = join(result, operator);
  	            result = [join(
  	                result,
  	                that.generateExpression(stmt.right, Precedence.Assignment, E_TTT)
  	            ), ')'];
  	        });
  	        result.push(this.maybeBlock(stmt.body, flags));
  	        return result;
  	    };

  	    CodeGenerator.prototype.generatePropertyKey = function (expr, computed) {
  	        var result = [];

  	        if (computed) {
  	            result.push('[');
  	        }

  	        result.push(this.generateExpression(expr, Precedence.Assignment, E_TTT));

  	        if (computed) {
  	            result.push(']');
  	        }

  	        return result;
  	    };

  	    CodeGenerator.prototype.generateAssignment = function (left, right, operator, precedence, flags) {
  	        if (Precedence.Assignment < precedence) {
  	            flags |= F_ALLOW_IN;
  	        }

  	        return parenthesize(
  	            [
  	                this.generateExpression(left, Precedence.Call, flags),
  	                space + operator + space,
  	                this.generateExpression(right, Precedence.Assignment, flags)
  	            ],
  	            Precedence.Assignment,
  	            precedence
  	        );
  	    };

  	    CodeGenerator.prototype.semicolon = function (flags) {
  	        if (!semicolons && flags & F_SEMICOLON_OPT) {
  	            return '';
  	        }
  	        return ';';
  	    };

  	    // Statements.

  	    CodeGenerator.Statement = {

  	        BlockStatement: function (stmt, flags) {
  	            var range, content, result = ['{', newline], that = this;

  	            withIndent(function () {
  	                // handle functions without any code
  	                if (stmt.body.length === 0 && preserveBlankLines) {
  	                    range = stmt.range;
  	                    if (range[1] - range[0] > 2) {
  	                        content = sourceCode.substring(range[0] + 1, range[1] - 1);
  	                        if (content[0] === '\n') {
  	                            result = ['{'];
  	                        }
  	                        result.push(content);
  	                    }
  	                }

  	                var i, iz, fragment, bodyFlags;
  	                bodyFlags = S_TFFF;
  	                if (flags & F_FUNC_BODY) {
  	                    bodyFlags |= F_DIRECTIVE_CTX;
  	                }

  	                for (i = 0, iz = stmt.body.length; i < iz; ++i) {
  	                    if (preserveBlankLines) {
  	                        // handle spaces before the first line
  	                        if (i === 0) {
  	                            if (stmt.body[0].leadingComments) {
  	                                range = stmt.body[0].leadingComments[0].extendedRange;
  	                                content = sourceCode.substring(range[0], range[1]);
  	                                if (content[0] === '\n') {
  	                                    result = ['{'];
  	                                }
  	                            }
  	                            if (!stmt.body[0].leadingComments) {
  	                                generateBlankLines(stmt.range[0], stmt.body[0].range[0], result);
  	                            }
  	                        }

  	                        // handle spaces between lines
  	                        if (i > 0) {
  	                            if (!stmt.body[i - 1].trailingComments  && !stmt.body[i].leadingComments) {
  	                                generateBlankLines(stmt.body[i - 1].range[1], stmt.body[i].range[0], result);
  	                            }
  	                        }
  	                    }

  	                    if (i === iz - 1) {
  	                        bodyFlags |= F_SEMICOLON_OPT;
  	                    }

  	                    if (stmt.body[i].leadingComments && preserveBlankLines) {
  	                        fragment = that.generateStatement(stmt.body[i], bodyFlags);
  	                    } else {
  	                        fragment = addIndent(that.generateStatement(stmt.body[i], bodyFlags));
  	                    }

  	                    result.push(fragment);
  	                    if (!endsWithLineTerminator(toSourceNodeWhenNeeded(fragment).toString())) {
  	                        if (preserveBlankLines && i < iz - 1) {
  	                            // don't add a new line if there are leading coments
  	                            // in the next statement
  	                            if (!stmt.body[i + 1].leadingComments) {
  	                                result.push(newline);
  	                            }
  	                        } else {
  	                            result.push(newline);
  	                        }
  	                    }

  	                    if (preserveBlankLines) {
  	                        // handle spaces after the last line
  	                        if (i === iz - 1) {
  	                            if (!stmt.body[i].trailingComments) {
  	                                generateBlankLines(stmt.body[i].range[1], stmt.range[1], result);
  	                            }
  	                        }
  	                    }
  	                }
  	            });

  	            result.push(addIndent('}'));
  	            return result;
  	        },

  	        BreakStatement: function (stmt, flags) {
  	            if (stmt.label) {
  	                return 'break ' + stmt.label.name + this.semicolon(flags);
  	            }
  	            return 'break' + this.semicolon(flags);
  	        },

  	        ContinueStatement: function (stmt, flags) {
  	            if (stmt.label) {
  	                return 'continue ' + stmt.label.name + this.semicolon(flags);
  	            }
  	            return 'continue' + this.semicolon(flags);
  	        },

  	        ClassBody: function (stmt, flags) {
  	            var result = [ '{', newline], that = this;

  	            withIndent(function (indent) {
  	                var i, iz;

  	                for (i = 0, iz = stmt.body.length; i < iz; ++i) {
  	                    result.push(indent);
  	                    result.push(that.generateExpression(stmt.body[i], Precedence.Sequence, E_TTT));
  	                    if (i + 1 < iz) {
  	                        result.push(newline);
  	                    }
  	                }
  	            });

  	            if (!endsWithLineTerminator(toSourceNodeWhenNeeded(result).toString())) {
  	                result.push(newline);
  	            }
  	            result.push(base);
  	            result.push('}');
  	            return result;
  	        },

  	        ClassDeclaration: function (stmt, flags) {
  	            var result, fragment;
  	            result  = ['class'];
  	            if (stmt.id) {
  	                result = join(result, this.generateExpression(stmt.id, Precedence.Sequence, E_TTT));
  	            }
  	            if (stmt.superClass) {
  	                fragment = join('extends', this.generateExpression(stmt.superClass, Precedence.Unary, E_TTT));
  	                result = join(result, fragment);
  	            }
  	            result.push(space);
  	            result.push(this.generateStatement(stmt.body, S_TFFT));
  	            return result;
  	        },

  	        DirectiveStatement: function (stmt, flags) {
  	            if (extra.raw && stmt.raw) {
  	                return stmt.raw + this.semicolon(flags);
  	            }
  	            return escapeDirective(stmt.directive) + this.semicolon(flags);
  	        },

  	        DoWhileStatement: function (stmt, flags) {
  	            // Because `do 42 while (cond)` is Syntax Error. We need semicolon.
  	            var result = join('do', this.maybeBlock(stmt.body, S_TFFF));
  	            result = this.maybeBlockSuffix(stmt.body, result);
  	            return join(result, [
  	                'while' + space + '(',
  	                this.generateExpression(stmt.test, Precedence.Sequence, E_TTT),
  	                ')' + this.semicolon(flags)
  	            ]);
  	        },

  	        CatchClause: function (stmt, flags) {
  	            var result, that = this;
  	            withIndent(function () {
  	                var guard;

  	                if (stmt.param) {
  	                    result = [
  	                        'catch' + space + '(',
  	                        that.generateExpression(stmt.param, Precedence.Sequence, E_TTT),
  	                        ')'
  	                    ];

  	                    if (stmt.guard) {
  	                        guard = that.generateExpression(stmt.guard, Precedence.Sequence, E_TTT);
  	                        result.splice(2, 0, ' if ', guard);
  	                    }
  	                } else {
  	                    result = ['catch'];
  	                }
  	            });
  	            result.push(this.maybeBlock(stmt.body, S_TFFF));
  	            return result;
  	        },

  	        DebuggerStatement: function (stmt, flags) {
  	            return 'debugger' + this.semicolon(flags);
  	        },

  	        EmptyStatement: function (stmt, flags) {
  	            return ';';
  	        },

  	        ExportDefaultDeclaration: function (stmt, flags) {
  	            var result = [ 'export' ], bodyFlags;

  	            bodyFlags = (flags & F_SEMICOLON_OPT) ? S_TFFT : S_TFFF;

  	            // export default HoistableDeclaration[Default]
  	            // export default AssignmentExpression[In] ;
  	            result = join(result, 'default');
  	            if (isStatement(stmt.declaration)) {
  	                result = join(result, this.generateStatement(stmt.declaration, bodyFlags));
  	            } else {
  	                result = join(result, this.generateExpression(stmt.declaration, Precedence.Assignment, E_TTT) + this.semicolon(flags));
  	            }
  	            return result;
  	        },

  	        ExportNamedDeclaration: function (stmt, flags) {
  	            var result = [ 'export' ], bodyFlags, that = this;

  	            bodyFlags = (flags & F_SEMICOLON_OPT) ? S_TFFT : S_TFFF;

  	            // export VariableStatement
  	            // export Declaration[Default]
  	            if (stmt.declaration) {
  	                return join(result, this.generateStatement(stmt.declaration, bodyFlags));
  	            }

  	            // export ExportClause[NoReference] FromClause ;
  	            // export ExportClause ;
  	            if (stmt.specifiers) {
  	                if (stmt.specifiers.length === 0) {
  	                    result = join(result, '{' + space + '}');
  	                } else if (stmt.specifiers[0].type === Syntax.ExportBatchSpecifier) {
  	                    result = join(result, this.generateExpression(stmt.specifiers[0], Precedence.Sequence, E_TTT));
  	                } else {
  	                    result = join(result, '{');
  	                    withIndent(function (indent) {
  	                        var i, iz;
  	                        result.push(newline);
  	                        for (i = 0, iz = stmt.specifiers.length; i < iz; ++i) {
  	                            result.push(indent);
  	                            result.push(that.generateExpression(stmt.specifiers[i], Precedence.Sequence, E_TTT));
  	                            if (i + 1 < iz) {
  	                                result.push(',' + newline);
  	                            }
  	                        }
  	                    });
  	                    if (!endsWithLineTerminator(toSourceNodeWhenNeeded(result).toString())) {
  	                        result.push(newline);
  	                    }
  	                    result.push(base + '}');
  	                }

  	                if (stmt.source) {
  	                    result = join(result, [
  	                        'from' + space,
  	                        // ModuleSpecifier
  	                        this.generateExpression(stmt.source, Precedence.Sequence, E_TTT),
  	                        this.semicolon(flags)
  	                    ]);
  	                } else {
  	                    result.push(this.semicolon(flags));
  	                }
  	            }
  	            return result;
  	        },

  	        ExportAllDeclaration: function (stmt, flags) {
  	            // export * FromClause ;
  	            return [
  	                'export' + space,
  	                '*' + space,
  	                'from' + space,
  	                // ModuleSpecifier
  	                this.generateExpression(stmt.source, Precedence.Sequence, E_TTT),
  	                this.semicolon(flags)
  	            ];
  	        },

  	        ExpressionStatement: function (stmt, flags) {
  	            var result, fragment;

  	            function isClassPrefixed(fragment) {
  	                var code;
  	                if (fragment.slice(0, 5) !== 'class') {
  	                    return false;
  	                }
  	                code = fragment.charCodeAt(5);
  	                return code === 0x7B  /* '{' */ || esutils.code.isWhiteSpace(code) || esutils.code.isLineTerminator(code);
  	            }

  	            function isFunctionPrefixed(fragment) {
  	                var code;
  	                if (fragment.slice(0, 8) !== 'function') {
  	                    return false;
  	                }
  	                code = fragment.charCodeAt(8);
  	                return code === 0x28 /* '(' */ || esutils.code.isWhiteSpace(code) || code === 0x2A  /* '*' */ || esutils.code.isLineTerminator(code);
  	            }

  	            function isAsyncPrefixed(fragment) {
  	                var code, i, iz;
  	                if (fragment.slice(0, 5) !== 'async') {
  	                    return false;
  	                }
  	                if (!esutils.code.isWhiteSpace(fragment.charCodeAt(5))) {
  	                    return false;
  	                }
  	                for (i = 6, iz = fragment.length; i < iz; ++i) {
  	                    if (!esutils.code.isWhiteSpace(fragment.charCodeAt(i))) {
  	                        break;
  	                    }
  	                }
  	                if (i === iz) {
  	                    return false;
  	                }
  	                if (fragment.slice(i, i + 8) !== 'function') {
  	                    return false;
  	                }
  	                code = fragment.charCodeAt(i + 8);
  	                return code === 0x28 /* '(' */ || esutils.code.isWhiteSpace(code) || code === 0x2A  /* '*' */ || esutils.code.isLineTerminator(code);
  	            }

  	            result = [this.generateExpression(stmt.expression, Precedence.Sequence, E_TTT)];
  	            // 12.4 '{', 'function', 'class' is not allowed in this position.
  	            // wrap expression with parentheses
  	            fragment = toSourceNodeWhenNeeded(result).toString();
  	            if (fragment.charCodeAt(0) === 0x7B  /* '{' */ ||  // ObjectExpression
  	                    isClassPrefixed(fragment) ||
  	                    isFunctionPrefixed(fragment) ||
  	                    isAsyncPrefixed(fragment) ||
  	                    (directive && (flags & F_DIRECTIVE_CTX) && stmt.expression.type === Syntax.Literal && typeof stmt.expression.value === 'string')) {
  	                result = ['(', result, ')' + this.semicolon(flags)];
  	            } else {
  	                result.push(this.semicolon(flags));
  	            }
  	            return result;
  	        },

  	        ImportDeclaration: function (stmt, flags) {
  	            // ES6: 15.2.1 valid import declarations:
  	            //     - import ImportClause FromClause ;
  	            //     - import ModuleSpecifier ;
  	            var result, cursor, that = this;

  	            // If no ImportClause is present,
  	            // this should be `import ModuleSpecifier` so skip `from`
  	            // ModuleSpecifier is StringLiteral.
  	            if (stmt.specifiers.length === 0) {
  	                // import ModuleSpecifier ;
  	                return [
  	                    'import',
  	                    space,
  	                    // ModuleSpecifier
  	                    this.generateExpression(stmt.source, Precedence.Sequence, E_TTT),
  	                    this.semicolon(flags)
  	                ];
  	            }

  	            // import ImportClause FromClause ;
  	            result = [
  	                'import'
  	            ];
  	            cursor = 0;

  	            // ImportedBinding
  	            if (stmt.specifiers[cursor].type === Syntax.ImportDefaultSpecifier) {
  	                result = join(result, [
  	                        this.generateExpression(stmt.specifiers[cursor], Precedence.Sequence, E_TTT)
  	                ]);
  	                ++cursor;
  	            }

  	            if (stmt.specifiers[cursor]) {
  	                if (cursor !== 0) {
  	                    result.push(',');
  	                }

  	                if (stmt.specifiers[cursor].type === Syntax.ImportNamespaceSpecifier) {
  	                    // NameSpaceImport
  	                    result = join(result, [
  	                            space,
  	                            this.generateExpression(stmt.specifiers[cursor], Precedence.Sequence, E_TTT)
  	                    ]);
  	                } else {
  	                    // NamedImports
  	                    result.push(space + '{');

  	                    if ((stmt.specifiers.length - cursor) === 1) {
  	                        // import { ... } from "...";
  	                        result.push(space);
  	                        result.push(this.generateExpression(stmt.specifiers[cursor], Precedence.Sequence, E_TTT));
  	                        result.push(space + '}' + space);
  	                    } else {
  	                        // import {
  	                        //    ...,
  	                        //    ...,
  	                        // } from "...";
  	                        withIndent(function (indent) {
  	                            var i, iz;
  	                            result.push(newline);
  	                            for (i = cursor, iz = stmt.specifiers.length; i < iz; ++i) {
  	                                result.push(indent);
  	                                result.push(that.generateExpression(stmt.specifiers[i], Precedence.Sequence, E_TTT));
  	                                if (i + 1 < iz) {
  	                                    result.push(',' + newline);
  	                                }
  	                            }
  	                        });
  	                        if (!endsWithLineTerminator(toSourceNodeWhenNeeded(result).toString())) {
  	                            result.push(newline);
  	                        }
  	                        result.push(base + '}' + space);
  	                    }
  	                }
  	            }

  	            result = join(result, [
  	                'from' + space,
  	                // ModuleSpecifier
  	                this.generateExpression(stmt.source, Precedence.Sequence, E_TTT),
  	                this.semicolon(flags)
  	            ]);
  	            return result;
  	        },

  	        VariableDeclarator: function (stmt, flags) {
  	            var itemFlags = (flags & F_ALLOW_IN) ? E_TTT : E_FTT;
  	            if (stmt.init) {
  	                return [
  	                    this.generateExpression(stmt.id, Precedence.Assignment, itemFlags),
  	                    space,
  	                    '=',
  	                    space,
  	                    this.generateExpression(stmt.init, Precedence.Assignment, itemFlags)
  	                ];
  	            }
  	            return this.generatePattern(stmt.id, Precedence.Assignment, itemFlags);
  	        },

  	        VariableDeclaration: function (stmt, flags) {
  	            // VariableDeclarator is typed as Statement,
  	            // but joined with comma (not LineTerminator).
  	            // So if comment is attached to target node, we should specialize.
  	            var result, i, iz, node, bodyFlags, that = this;

  	            result = [ stmt.kind ];

  	            bodyFlags = (flags & F_ALLOW_IN) ? S_TFFF : S_FFFF;

  	            function block() {
  	                node = stmt.declarations[0];
  	                if (extra.comment && node.leadingComments) {
  	                    result.push('\n');
  	                    result.push(addIndent(that.generateStatement(node, bodyFlags)));
  	                } else {
  	                    result.push(noEmptySpace());
  	                    result.push(that.generateStatement(node, bodyFlags));
  	                }

  	                for (i = 1, iz = stmt.declarations.length; i < iz; ++i) {
  	                    node = stmt.declarations[i];
  	                    if (extra.comment && node.leadingComments) {
  	                        result.push(',' + newline);
  	                        result.push(addIndent(that.generateStatement(node, bodyFlags)));
  	                    } else {
  	                        result.push(',' + space);
  	                        result.push(that.generateStatement(node, bodyFlags));
  	                    }
  	                }
  	            }

  	            if (stmt.declarations.length > 1) {
  	                withIndent(block);
  	            } else {
  	                block();
  	            }

  	            result.push(this.semicolon(flags));

  	            return result;
  	        },

  	        ThrowStatement: function (stmt, flags) {
  	            return [join(
  	                'throw',
  	                this.generateExpression(stmt.argument, Precedence.Sequence, E_TTT)
  	            ), this.semicolon(flags)];
  	        },

  	        TryStatement: function (stmt, flags) {
  	            var result, i, iz, guardedHandlers;

  	            result = ['try', this.maybeBlock(stmt.block, S_TFFF)];
  	            result = this.maybeBlockSuffix(stmt.block, result);

  	            if (stmt.handlers) {
  	                // old interface
  	                for (i = 0, iz = stmt.handlers.length; i < iz; ++i) {
  	                    result = join(result, this.generateStatement(stmt.handlers[i], S_TFFF));
  	                    if (stmt.finalizer || i + 1 !== iz) {
  	                        result = this.maybeBlockSuffix(stmt.handlers[i].body, result);
  	                    }
  	                }
  	            } else {
  	                guardedHandlers = stmt.guardedHandlers || [];

  	                for (i = 0, iz = guardedHandlers.length; i < iz; ++i) {
  	                    result = join(result, this.generateStatement(guardedHandlers[i], S_TFFF));
  	                    if (stmt.finalizer || i + 1 !== iz) {
  	                        result = this.maybeBlockSuffix(guardedHandlers[i].body, result);
  	                    }
  	                }

  	                // new interface
  	                if (stmt.handler) {
  	                    if (Array.isArray(stmt.handler)) {
  	                        for (i = 0, iz = stmt.handler.length; i < iz; ++i) {
  	                            result = join(result, this.generateStatement(stmt.handler[i], S_TFFF));
  	                            if (stmt.finalizer || i + 1 !== iz) {
  	                                result = this.maybeBlockSuffix(stmt.handler[i].body, result);
  	                            }
  	                        }
  	                    } else {
  	                        result = join(result, this.generateStatement(stmt.handler, S_TFFF));
  	                        if (stmt.finalizer) {
  	                            result = this.maybeBlockSuffix(stmt.handler.body, result);
  	                        }
  	                    }
  	                }
  	            }
  	            if (stmt.finalizer) {
  	                result = join(result, ['finally', this.maybeBlock(stmt.finalizer, S_TFFF)]);
  	            }
  	            return result;
  	        },

  	        SwitchStatement: function (stmt, flags) {
  	            var result, fragment, i, iz, bodyFlags, that = this;
  	            withIndent(function () {
  	                result = [
  	                    'switch' + space + '(',
  	                    that.generateExpression(stmt.discriminant, Precedence.Sequence, E_TTT),
  	                    ')' + space + '{' + newline
  	                ];
  	            });
  	            if (stmt.cases) {
  	                bodyFlags = S_TFFF;
  	                for (i = 0, iz = stmt.cases.length; i < iz; ++i) {
  	                    if (i === iz - 1) {
  	                        bodyFlags |= F_SEMICOLON_OPT;
  	                    }
  	                    fragment = addIndent(this.generateStatement(stmt.cases[i], bodyFlags));
  	                    result.push(fragment);
  	                    if (!endsWithLineTerminator(toSourceNodeWhenNeeded(fragment).toString())) {
  	                        result.push(newline);
  	                    }
  	                }
  	            }
  	            result.push(addIndent('}'));
  	            return result;
  	        },

  	        SwitchCase: function (stmt, flags) {
  	            var result, fragment, i, iz, bodyFlags, that = this;
  	            withIndent(function () {
  	                if (stmt.test) {
  	                    result = [
  	                        join('case', that.generateExpression(stmt.test, Precedence.Sequence, E_TTT)),
  	                        ':'
  	                    ];
  	                } else {
  	                    result = ['default:'];
  	                }

  	                i = 0;
  	                iz = stmt.consequent.length;
  	                if (iz && stmt.consequent[0].type === Syntax.BlockStatement) {
  	                    fragment = that.maybeBlock(stmt.consequent[0], S_TFFF);
  	                    result.push(fragment);
  	                    i = 1;
  	                }

  	                if (i !== iz && !endsWithLineTerminator(toSourceNodeWhenNeeded(result).toString())) {
  	                    result.push(newline);
  	                }

  	                bodyFlags = S_TFFF;
  	                for (; i < iz; ++i) {
  	                    if (i === iz - 1 && flags & F_SEMICOLON_OPT) {
  	                        bodyFlags |= F_SEMICOLON_OPT;
  	                    }
  	                    fragment = addIndent(that.generateStatement(stmt.consequent[i], bodyFlags));
  	                    result.push(fragment);
  	                    if (i + 1 !== iz && !endsWithLineTerminator(toSourceNodeWhenNeeded(fragment).toString())) {
  	                        result.push(newline);
  	                    }
  	                }
  	            });
  	            return result;
  	        },

  	        IfStatement: function (stmt, flags) {
  	            var result, bodyFlags, semicolonOptional, that = this;
  	            withIndent(function () {
  	                result = [
  	                    'if' + space + '(',
  	                    that.generateExpression(stmt.test, Precedence.Sequence, E_TTT),
  	                    ')'
  	                ];
  	            });
  	            semicolonOptional = flags & F_SEMICOLON_OPT;
  	            bodyFlags = S_TFFF;
  	            if (semicolonOptional) {
  	                bodyFlags |= F_SEMICOLON_OPT;
  	            }
  	            if (stmt.alternate) {
  	                result.push(this.maybeBlock(stmt.consequent, S_TFFF));
  	                result = this.maybeBlockSuffix(stmt.consequent, result);
  	                if (stmt.alternate.type === Syntax.IfStatement) {
  	                    result = join(result, ['else ', this.generateStatement(stmt.alternate, bodyFlags)]);
  	                } else {
  	                    result = join(result, join('else', this.maybeBlock(stmt.alternate, bodyFlags)));
  	                }
  	            } else {
  	                result.push(this.maybeBlock(stmt.consequent, bodyFlags));
  	            }
  	            return result;
  	        },

  	        ForStatement: function (stmt, flags) {
  	            var result, that = this;
  	            withIndent(function () {
  	                result = ['for' + space + '('];
  	                if (stmt.init) {
  	                    if (stmt.init.type === Syntax.VariableDeclaration) {
  	                        result.push(that.generateStatement(stmt.init, S_FFFF));
  	                    } else {
  	                        // F_ALLOW_IN becomes false.
  	                        result.push(that.generateExpression(stmt.init, Precedence.Sequence, E_FTT));
  	                        result.push(';');
  	                    }
  	                } else {
  	                    result.push(';');
  	                }

  	                if (stmt.test) {
  	                    result.push(space);
  	                    result.push(that.generateExpression(stmt.test, Precedence.Sequence, E_TTT));
  	                    result.push(';');
  	                } else {
  	                    result.push(';');
  	                }

  	                if (stmt.update) {
  	                    result.push(space);
  	                    result.push(that.generateExpression(stmt.update, Precedence.Sequence, E_TTT));
  	                    result.push(')');
  	                } else {
  	                    result.push(')');
  	                }
  	            });

  	            result.push(this.maybeBlock(stmt.body, flags & F_SEMICOLON_OPT ? S_TFFT : S_TFFF));
  	            return result;
  	        },

  	        ForInStatement: function (stmt, flags) {
  	            return this.generateIterationForStatement('in', stmt, flags & F_SEMICOLON_OPT ? S_TFFT : S_TFFF);
  	        },

  	        ForOfStatement: function (stmt, flags) {
  	            return this.generateIterationForStatement('of', stmt, flags & F_SEMICOLON_OPT ? S_TFFT : S_TFFF);
  	        },

  	        LabeledStatement: function (stmt, flags) {
  	            return [stmt.label.name + ':', this.maybeBlock(stmt.body, flags & F_SEMICOLON_OPT ? S_TFFT : S_TFFF)];
  	        },

  	        Program: function (stmt, flags) {
  	            var result, fragment, i, iz, bodyFlags;
  	            iz = stmt.body.length;
  	            result = [safeConcatenation && iz > 0 ? '\n' : ''];
  	            bodyFlags = S_TFTF;
  	            for (i = 0; i < iz; ++i) {
  	                if (!safeConcatenation && i === iz - 1) {
  	                    bodyFlags |= F_SEMICOLON_OPT;
  	                }

  	                if (preserveBlankLines) {
  	                    // handle spaces before the first line
  	                    if (i === 0) {
  	                        if (!stmt.body[0].leadingComments) {
  	                            generateBlankLines(stmt.range[0], stmt.body[i].range[0], result);
  	                        }
  	                    }

  	                    // handle spaces between lines
  	                    if (i > 0) {
  	                        if (!stmt.body[i - 1].trailingComments && !stmt.body[i].leadingComments) {
  	                            generateBlankLines(stmt.body[i - 1].range[1], stmt.body[i].range[0], result);
  	                        }
  	                    }
  	                }

  	                fragment = addIndent(this.generateStatement(stmt.body[i], bodyFlags));
  	                result.push(fragment);
  	                if (i + 1 < iz && !endsWithLineTerminator(toSourceNodeWhenNeeded(fragment).toString())) {
  	                    if (preserveBlankLines) {
  	                        if (!stmt.body[i + 1].leadingComments) {
  	                            result.push(newline);
  	                        }
  	                    } else {
  	                        result.push(newline);
  	                    }
  	                }

  	                if (preserveBlankLines) {
  	                    // handle spaces after the last line
  	                    if (i === iz - 1) {
  	                        if (!stmt.body[i].trailingComments) {
  	                            generateBlankLines(stmt.body[i].range[1], stmt.range[1], result);
  	                        }
  	                    }
  	                }
  	            }
  	            return result;
  	        },

  	        FunctionDeclaration: function (stmt, flags) {
  	            return [
  	                generateAsyncPrefix(stmt, true),
  	                'function',
  	                generateStarSuffix(stmt) || noEmptySpace(),
  	                stmt.id ? generateIdentifier(stmt.id) : '',
  	                this.generateFunctionBody(stmt)
  	            ];
  	        },

  	        ReturnStatement: function (stmt, flags) {
  	            if (stmt.argument) {
  	                return [join(
  	                    'return',
  	                    this.generateExpression(stmt.argument, Precedence.Sequence, E_TTT)
  	                ), this.semicolon(flags)];
  	            }
  	            return ['return' + this.semicolon(flags)];
  	        },

  	        WhileStatement: function (stmt, flags) {
  	            var result, that = this;
  	            withIndent(function () {
  	                result = [
  	                    'while' + space + '(',
  	                    that.generateExpression(stmt.test, Precedence.Sequence, E_TTT),
  	                    ')'
  	                ];
  	            });
  	            result.push(this.maybeBlock(stmt.body, flags & F_SEMICOLON_OPT ? S_TFFT : S_TFFF));
  	            return result;
  	        },

  	        WithStatement: function (stmt, flags) {
  	            var result, that = this;
  	            withIndent(function () {
  	                result = [
  	                    'with' + space + '(',
  	                    that.generateExpression(stmt.object, Precedence.Sequence, E_TTT),
  	                    ')'
  	                ];
  	            });
  	            result.push(this.maybeBlock(stmt.body, flags & F_SEMICOLON_OPT ? S_TFFT : S_TFFF));
  	            return result;
  	        }

  	    };

  	    merge(CodeGenerator.prototype, CodeGenerator.Statement);

  	    // Expressions.

  	    CodeGenerator.Expression = {

  	        SequenceExpression: function (expr, precedence, flags) {
  	            var result, i, iz;
  	            if (Precedence.Sequence < precedence) {
  	                flags |= F_ALLOW_IN;
  	            }
  	            result = [];
  	            for (i = 0, iz = expr.expressions.length; i < iz; ++i) {
  	                result.push(this.generateExpression(expr.expressions[i], Precedence.Assignment, flags));
  	                if (i + 1 < iz) {
  	                    result.push(',' + space);
  	                }
  	            }
  	            return parenthesize(result, Precedence.Sequence, precedence);
  	        },

  	        AssignmentExpression: function (expr, precedence, flags) {
  	            return this.generateAssignment(expr.left, expr.right, expr.operator, precedence, flags);
  	        },

  	        ArrowFunctionExpression: function (expr, precedence, flags) {
  	            return parenthesize(this.generateFunctionBody(expr), Precedence.ArrowFunction, precedence);
  	        },

  	        ConditionalExpression: function (expr, precedence, flags) {
  	            if (Precedence.Conditional < precedence) {
  	                flags |= F_ALLOW_IN;
  	            }
  	            return parenthesize(
  	                [
  	                    this.generateExpression(expr.test, Precedence.Coalesce, flags),
  	                    space + '?' + space,
  	                    this.generateExpression(expr.consequent, Precedence.Assignment, flags),
  	                    space + ':' + space,
  	                    this.generateExpression(expr.alternate, Precedence.Assignment, flags)
  	                ],
  	                Precedence.Conditional,
  	                precedence
  	            );
  	        },

  	        LogicalExpression: function (expr, precedence, flags) {
  	            if (expr.operator === '??') {
  	                flags |= F_FOUND_COALESCE;
  	            }
  	            return this.BinaryExpression(expr, precedence, flags);
  	        },

  	        BinaryExpression: function (expr, precedence, flags) {
  	            var result, leftPrecedence, rightPrecedence, currentPrecedence, fragment, leftSource;
  	            currentPrecedence = BinaryPrecedence[expr.operator];
  	            leftPrecedence = expr.operator === '**' ? Precedence.Postfix : currentPrecedence;
  	            rightPrecedence = expr.operator === '**' ? currentPrecedence : currentPrecedence + 1;

  	            if (currentPrecedence < precedence) {
  	                flags |= F_ALLOW_IN;
  	            }

  	            fragment = this.generateExpression(expr.left, leftPrecedence, flags);

  	            leftSource = fragment.toString();

  	            if (leftSource.charCodeAt(leftSource.length - 1) === 0x2F /* / */ && esutils.code.isIdentifierPartES5(expr.operator.charCodeAt(0))) {
  	                result = [fragment, noEmptySpace(), expr.operator];
  	            } else {
  	                result = join(fragment, expr.operator);
  	            }

  	            fragment = this.generateExpression(expr.right, rightPrecedence, flags);

  	            if (expr.operator === '/' && fragment.toString().charAt(0) === '/' ||
  	            expr.operator.slice(-1) === '<' && fragment.toString().slice(0, 3) === '!--') {
  	                // If '/' concats with '/' or `<` concats with `!--`, it is interpreted as comment start
  	                result.push(noEmptySpace());
  	                result.push(fragment);
  	            } else {
  	                result = join(result, fragment);
  	            }

  	            if (expr.operator === 'in' && !(flags & F_ALLOW_IN)) {
  	                return ['(', result, ')'];
  	            }
  	            if ((expr.operator === '||' || expr.operator === '&&') && (flags & F_FOUND_COALESCE)) {
  	                return ['(', result, ')'];
  	            }
  	            return parenthesize(result, currentPrecedence, precedence);
  	        },

  	        CallExpression: function (expr, precedence, flags) {
  	            var result, i, iz;

  	            // F_ALLOW_UNPARATH_NEW becomes false.
  	            result = [this.generateExpression(expr.callee, Precedence.Call, E_TTF)];

  	            if (expr.optional) {
  	                result.push('?.');
  	            }

  	            result.push('(');
  	            for (i = 0, iz = expr['arguments'].length; i < iz; ++i) {
  	                result.push(this.generateExpression(expr['arguments'][i], Precedence.Assignment, E_TTT));
  	                if (i + 1 < iz) {
  	                    result.push(',' + space);
  	                }
  	            }
  	            result.push(')');

  	            if (!(flags & F_ALLOW_CALL)) {
  	                return ['(', result, ')'];
  	            }

  	            return parenthesize(result, Precedence.Call, precedence);
  	        },

  	        ChainExpression: function (expr, precedence, flags) {
  	            if (Precedence.OptionalChaining < precedence) {
  	                flags |= F_ALLOW_CALL;
  	            }

  	            var result = this.generateExpression(expr.expression, Precedence.OptionalChaining, flags);

  	            return parenthesize(result, Precedence.OptionalChaining, precedence);
  	        },

  	        NewExpression: function (expr, precedence, flags) {
  	            var result, length, i, iz, itemFlags;
  	            length = expr['arguments'].length;

  	            // F_ALLOW_CALL becomes false.
  	            // F_ALLOW_UNPARATH_NEW may become false.
  	            itemFlags = (flags & F_ALLOW_UNPARATH_NEW && !parentheses && length === 0) ? E_TFT : E_TFF;

  	            result = join(
  	                'new',
  	                this.generateExpression(expr.callee, Precedence.New, itemFlags)
  	            );

  	            if (!(flags & F_ALLOW_UNPARATH_NEW) || parentheses || length > 0) {
  	                result.push('(');
  	                for (i = 0, iz = length; i < iz; ++i) {
  	                    result.push(this.generateExpression(expr['arguments'][i], Precedence.Assignment, E_TTT));
  	                    if (i + 1 < iz) {
  	                        result.push(',' + space);
  	                    }
  	                }
  	                result.push(')');
  	            }

  	            return parenthesize(result, Precedence.New, precedence);
  	        },

  	        MemberExpression: function (expr, precedence, flags) {
  	            var result, fragment;

  	            // F_ALLOW_UNPARATH_NEW becomes false.
  	            result = [this.generateExpression(expr.object, Precedence.Call, (flags & F_ALLOW_CALL) ? E_TTF : E_TFF)];

  	            if (expr.computed) {
  	                if (expr.optional) {
  	                    result.push('?.');
  	                }

  	                result.push('[');
  	                result.push(this.generateExpression(expr.property, Precedence.Sequence, flags & F_ALLOW_CALL ? E_TTT : E_TFT));
  	                result.push(']');
  	            } else {
  	                if (!expr.optional && expr.object.type === Syntax.Literal && typeof expr.object.value === 'number') {
  	                    fragment = toSourceNodeWhenNeeded(result).toString();
  	                    // When the following conditions are all true,
  	                    //   1. No floating point
  	                    //   2. Don't have exponents
  	                    //   3. The last character is a decimal digit
  	                    //   4. Not hexadecimal OR octal number literal
  	                    // we should add a floating point.
  	                    if (
  	                            fragment.indexOf('.') < 0 &&
  	                            !/[eExX]/.test(fragment) &&
  	                            esutils.code.isDecimalDigit(fragment.charCodeAt(fragment.length - 1)) &&
  	                            !(fragment.length >= 2 && fragment.charCodeAt(0) === 48)  // '0'
  	                            ) {
  	                        result.push(' ');
  	                    }
  	                }
  	                result.push(expr.optional ? '?.' : '.');
  	                result.push(generateIdentifier(expr.property));
  	            }

  	            return parenthesize(result, Precedence.Member, precedence);
  	        },

  	        MetaProperty: function (expr, precedence, flags) {
  	            var result;
  	            result = [];
  	            result.push(typeof expr.meta === "string" ? expr.meta : generateIdentifier(expr.meta));
  	            result.push('.');
  	            result.push(typeof expr.property === "string" ? expr.property : generateIdentifier(expr.property));
  	            return parenthesize(result, Precedence.Member, precedence);
  	        },

  	        UnaryExpression: function (expr, precedence, flags) {
  	            var result, fragment, rightCharCode, leftSource, leftCharCode;
  	            fragment = this.generateExpression(expr.argument, Precedence.Unary, E_TTT);

  	            if (space === '') {
  	                result = join(expr.operator, fragment);
  	            } else {
  	                result = [expr.operator];
  	                if (expr.operator.length > 2) {
  	                    // delete, void, typeof
  	                    // get `typeof []`, not `typeof[]`
  	                    result = join(result, fragment);
  	                } else {
  	                    // Prevent inserting spaces between operator and argument if it is unnecessary
  	                    // like, `!cond`
  	                    leftSource = toSourceNodeWhenNeeded(result).toString();
  	                    leftCharCode = leftSource.charCodeAt(leftSource.length - 1);
  	                    rightCharCode = fragment.toString().charCodeAt(0);

  	                    if (((leftCharCode === 0x2B  /* + */ || leftCharCode === 0x2D  /* - */) && leftCharCode === rightCharCode) ||
  	                            (esutils.code.isIdentifierPartES5(leftCharCode) && esutils.code.isIdentifierPartES5(rightCharCode))) {
  	                        result.push(noEmptySpace());
  	                        result.push(fragment);
  	                    } else {
  	                        result.push(fragment);
  	                    }
  	                }
  	            }
  	            return parenthesize(result, Precedence.Unary, precedence);
  	        },

  	        YieldExpression: function (expr, precedence, flags) {
  	            var result;
  	            if (expr.delegate) {
  	                result = 'yield*';
  	            } else {
  	                result = 'yield';
  	            }
  	            if (expr.argument) {
  	                result = join(
  	                    result,
  	                    this.generateExpression(expr.argument, Precedence.Yield, E_TTT)
  	                );
  	            }
  	            return parenthesize(result, Precedence.Yield, precedence);
  	        },

  	        AwaitExpression: function (expr, precedence, flags) {
  	            var result = join(
  	                expr.all ? 'await*' : 'await',
  	                this.generateExpression(expr.argument, Precedence.Await, E_TTT)
  	            );
  	            return parenthesize(result, Precedence.Await, precedence);
  	        },

  	        UpdateExpression: function (expr, precedence, flags) {
  	            if (expr.prefix) {
  	                return parenthesize(
  	                    [
  	                        expr.operator,
  	                        this.generateExpression(expr.argument, Precedence.Unary, E_TTT)
  	                    ],
  	                    Precedence.Unary,
  	                    precedence
  	                );
  	            }
  	            return parenthesize(
  	                [
  	                    this.generateExpression(expr.argument, Precedence.Postfix, E_TTT),
  	                    expr.operator
  	                ],
  	                Precedence.Postfix,
  	                precedence
  	            );
  	        },

  	        FunctionExpression: function (expr, precedence, flags) {
  	            var result = [
  	                generateAsyncPrefix(expr, true),
  	                'function'
  	            ];
  	            if (expr.id) {
  	                result.push(generateStarSuffix(expr) || noEmptySpace());
  	                result.push(generateIdentifier(expr.id));
  	            } else {
  	                result.push(generateStarSuffix(expr) || space);
  	            }
  	            result.push(this.generateFunctionBody(expr));
  	            return result;
  	        },

  	        ArrayPattern: function (expr, precedence, flags) {
  	            return this.ArrayExpression(expr, precedence, flags, true);
  	        },

  	        ArrayExpression: function (expr, precedence, flags, isPattern) {
  	            var result, multiline, that = this;
  	            if (!expr.elements.length) {
  	                return '[]';
  	            }
  	            multiline = isPattern ? false : expr.elements.length > 1;
  	            result = ['[', multiline ? newline : ''];
  	            withIndent(function (indent) {
  	                var i, iz;
  	                for (i = 0, iz = expr.elements.length; i < iz; ++i) {
  	                    if (!expr.elements[i]) {
  	                        if (multiline) {
  	                            result.push(indent);
  	                        }
  	                        if (i + 1 === iz) {
  	                            result.push(',');
  	                        }
  	                    } else {
  	                        result.push(multiline ? indent : '');
  	                        result.push(that.generateExpression(expr.elements[i], Precedence.Assignment, E_TTT));
  	                    }
  	                    if (i + 1 < iz) {
  	                        result.push(',' + (multiline ? newline : space));
  	                    }
  	                }
  	            });
  	            if (multiline && !endsWithLineTerminator(toSourceNodeWhenNeeded(result).toString())) {
  	                result.push(newline);
  	            }
  	            result.push(multiline ? base : '');
  	            result.push(']');
  	            return result;
  	        },

  	        RestElement: function(expr, precedence, flags) {
  	            return '...' + this.generatePattern(expr.argument);
  	        },

  	        ClassExpression: function (expr, precedence, flags) {
  	            var result, fragment;
  	            result = ['class'];
  	            if (expr.id) {
  	                result = join(result, this.generateExpression(expr.id, Precedence.Sequence, E_TTT));
  	            }
  	            if (expr.superClass) {
  	                fragment = join('extends', this.generateExpression(expr.superClass, Precedence.Unary, E_TTT));
  	                result = join(result, fragment);
  	            }
  	            result.push(space);
  	            result.push(this.generateStatement(expr.body, S_TFFT));
  	            return result;
  	        },

  	        MethodDefinition: function (expr, precedence, flags) {
  	            var result, fragment;
  	            if (expr['static']) {
  	                result = ['static' + space];
  	            } else {
  	                result = [];
  	            }
  	            if (expr.kind === 'get' || expr.kind === 'set') {
  	                fragment = [
  	                    join(expr.kind, this.generatePropertyKey(expr.key, expr.computed)),
  	                    this.generateFunctionBody(expr.value)
  	                ];
  	            } else {
  	                fragment = [
  	                    generateMethodPrefix(expr),
  	                    this.generatePropertyKey(expr.key, expr.computed),
  	                    this.generateFunctionBody(expr.value)
  	                ];
  	            }
  	            return join(result, fragment);
  	        },

  	        Property: function (expr, precedence, flags) {
  	            if (expr.kind === 'get' || expr.kind === 'set') {
  	                return [
  	                    expr.kind, noEmptySpace(),
  	                    this.generatePropertyKey(expr.key, expr.computed),
  	                    this.generateFunctionBody(expr.value)
  	                ];
  	            }

  	            if (expr.shorthand) {
  	                if (expr.value.type === "AssignmentPattern") {
  	                    return this.AssignmentPattern(expr.value, Precedence.Sequence, E_TTT);
  	                }
  	                return this.generatePropertyKey(expr.key, expr.computed);
  	            }

  	            if (expr.method) {
  	                return [
  	                    generateMethodPrefix(expr),
  	                    this.generatePropertyKey(expr.key, expr.computed),
  	                    this.generateFunctionBody(expr.value)
  	                ];
  	            }

  	            return [
  	                this.generatePropertyKey(expr.key, expr.computed),
  	                ':' + space,
  	                this.generateExpression(expr.value, Precedence.Assignment, E_TTT)
  	            ];
  	        },

  	        ObjectExpression: function (expr, precedence, flags) {
  	            var multiline, result, fragment, that = this;

  	            if (!expr.properties.length) {
  	                return '{}';
  	            }
  	            multiline = expr.properties.length > 1;

  	            withIndent(function () {
  	                fragment = that.generateExpression(expr.properties[0], Precedence.Sequence, E_TTT);
  	            });

  	            if (!multiline) {
  	                // issues 4
  	                // Do not transform from
  	                //   dejavu.Class.declare({
  	                //       method2: function () {}
  	                //   });
  	                // to
  	                //   dejavu.Class.declare({method2: function () {
  	                //       }});
  	                if (!hasLineTerminator(toSourceNodeWhenNeeded(fragment).toString())) {
  	                    return [ '{', space, fragment, space, '}' ];
  	                }
  	            }

  	            withIndent(function (indent) {
  	                var i, iz;
  	                result = [ '{', newline, indent, fragment ];

  	                if (multiline) {
  	                    result.push(',' + newline);
  	                    for (i = 1, iz = expr.properties.length; i < iz; ++i) {
  	                        result.push(indent);
  	                        result.push(that.generateExpression(expr.properties[i], Precedence.Sequence, E_TTT));
  	                        if (i + 1 < iz) {
  	                            result.push(',' + newline);
  	                        }
  	                    }
  	                }
  	            });

  	            if (!endsWithLineTerminator(toSourceNodeWhenNeeded(result).toString())) {
  	                result.push(newline);
  	            }
  	            result.push(base);
  	            result.push('}');
  	            return result;
  	        },

  	        AssignmentPattern: function(expr, precedence, flags) {
  	            return this.generateAssignment(expr.left, expr.right, '=', precedence, flags);
  	        },

  	        ObjectPattern: function (expr, precedence, flags) {
  	            var result, i, iz, multiline, property, that = this;
  	            if (!expr.properties.length) {
  	                return '{}';
  	            }

  	            multiline = false;
  	            if (expr.properties.length === 1) {
  	                property = expr.properties[0];
  	                if (
  	                    property.type === Syntax.Property
  	                    && property.value.type !== Syntax.Identifier
  	                ) {
  	                    multiline = true;
  	                }
  	            } else {
  	                for (i = 0, iz = expr.properties.length; i < iz; ++i) {
  	                    property = expr.properties[i];
  	                    if (
  	                        property.type === Syntax.Property
  	                        && !property.shorthand
  	                    ) {
  	                        multiline = true;
  	                        break;
  	                    }
  	                }
  	            }
  	            result = ['{', multiline ? newline : '' ];

  	            withIndent(function (indent) {
  	                var i, iz;
  	                for (i = 0, iz = expr.properties.length; i < iz; ++i) {
  	                    result.push(multiline ? indent : '');
  	                    result.push(that.generateExpression(expr.properties[i], Precedence.Sequence, E_TTT));
  	                    if (i + 1 < iz) {
  	                        result.push(',' + (multiline ? newline : space));
  	                    }
  	                }
  	            });

  	            if (multiline && !endsWithLineTerminator(toSourceNodeWhenNeeded(result).toString())) {
  	                result.push(newline);
  	            }
  	            result.push(multiline ? base : '');
  	            result.push('}');
  	            return result;
  	        },

  	        ThisExpression: function (expr, precedence, flags) {
  	            return 'this';
  	        },

  	        Super: function (expr, precedence, flags) {
  	            return 'super';
  	        },

  	        Identifier: function (expr, precedence, flags) {
  	            return generateIdentifier(expr);
  	        },

  	        ImportDefaultSpecifier: function (expr, precedence, flags) {
  	            return generateIdentifier(expr.id || expr.local);
  	        },

  	        ImportNamespaceSpecifier: function (expr, precedence, flags) {
  	            var result = ['*'];
  	            var id = expr.id || expr.local;
  	            if (id) {
  	                result.push(space + 'as' + noEmptySpace() + generateIdentifier(id));
  	            }
  	            return result;
  	        },

  	        ImportSpecifier: function (expr, precedence, flags) {
  	            var imported = expr.imported;
  	            var result = [ imported.name ];
  	            var local = expr.local;
  	            if (local && local.name !== imported.name) {
  	                result.push(noEmptySpace() + 'as' + noEmptySpace() + generateIdentifier(local));
  	            }
  	            return result;
  	        },

  	        ExportSpecifier: function (expr, precedence, flags) {
  	            var local = expr.local;
  	            var result = [ local.name ];
  	            var exported = expr.exported;
  	            if (exported && exported.name !== local.name) {
  	                result.push(noEmptySpace() + 'as' + noEmptySpace() + generateIdentifier(exported));
  	            }
  	            return result;
  	        },

  	        Literal: function (expr, precedence, flags) {
  	            var raw;
  	            if (expr.hasOwnProperty('raw') && parse && extra.raw) {
  	                try {
  	                    raw = parse(expr.raw).body[0].expression;
  	                    if (raw.type === Syntax.Literal) {
  	                        if (raw.value === expr.value) {
  	                            return expr.raw;
  	                        }
  	                    }
  	                } catch (e) {
  	                    // not use raw property
  	                }
  	            }

  	            if (expr.regex) {
  	              return '/' + expr.regex.pattern + '/' + expr.regex.flags;
  	            }

  	            if (typeof expr.value === 'bigint') {
  	                return expr.value.toString() + 'n';
  	            }

  	            // `expr.value` can be null if `expr.bigint` exists. We need to check
  	            // `expr.bigint` first.
  	            if (expr.bigint) {
  	                return expr.bigint + 'n';
  	            }

  	            if (expr.value === null) {
  	                return 'null';
  	            }

  	            if (typeof expr.value === 'string') {
  	                return escapeString(expr.value);
  	            }

  	            if (typeof expr.value === 'number') {
  	                return generateNumber(expr.value);
  	            }

  	            if (typeof expr.value === 'boolean') {
  	                return expr.value ? 'true' : 'false';
  	            }

  	            return generateRegExp(expr.value);
  	        },

  	        GeneratorExpression: function (expr, precedence, flags) {
  	            return this.ComprehensionExpression(expr, precedence, flags);
  	        },

  	        ComprehensionExpression: function (expr, precedence, flags) {
  	            // GeneratorExpression should be parenthesized with (...), ComprehensionExpression with [...]
  	            // Due to https://bugzilla.mozilla.org/show_bug.cgi?id=883468 position of expr.body can differ in Spidermonkey and ES6

  	            var result, i, iz, fragment, that = this;
  	            result = (expr.type === Syntax.GeneratorExpression) ? ['('] : ['['];

  	            if (extra.moz.comprehensionExpressionStartsWithAssignment) {
  	                fragment = this.generateExpression(expr.body, Precedence.Assignment, E_TTT);
  	                result.push(fragment);
  	            }

  	            if (expr.blocks) {
  	                withIndent(function () {
  	                    for (i = 0, iz = expr.blocks.length; i < iz; ++i) {
  	                        fragment = that.generateExpression(expr.blocks[i], Precedence.Sequence, E_TTT);
  	                        if (i > 0 || extra.moz.comprehensionExpressionStartsWithAssignment) {
  	                            result = join(result, fragment);
  	                        } else {
  	                            result.push(fragment);
  	                        }
  	                    }
  	                });
  	            }

  	            if (expr.filter) {
  	                result = join(result, 'if' + space);
  	                fragment = this.generateExpression(expr.filter, Precedence.Sequence, E_TTT);
  	                result = join(result, [ '(', fragment, ')' ]);
  	            }

  	            if (!extra.moz.comprehensionExpressionStartsWithAssignment) {
  	                fragment = this.generateExpression(expr.body, Precedence.Assignment, E_TTT);

  	                result = join(result, fragment);
  	            }

  	            result.push((expr.type === Syntax.GeneratorExpression) ? ')' : ']');
  	            return result;
  	        },

  	        ComprehensionBlock: function (expr, precedence, flags) {
  	            var fragment;
  	            if (expr.left.type === Syntax.VariableDeclaration) {
  	                fragment = [
  	                    expr.left.kind, noEmptySpace(),
  	                    this.generateStatement(expr.left.declarations[0], S_FFFF)
  	                ];
  	            } else {
  	                fragment = this.generateExpression(expr.left, Precedence.Call, E_TTT);
  	            }

  	            fragment = join(fragment, expr.of ? 'of' : 'in');
  	            fragment = join(fragment, this.generateExpression(expr.right, Precedence.Sequence, E_TTT));

  	            return [ 'for' + space + '(', fragment, ')' ];
  	        },

  	        SpreadElement: function (expr, precedence, flags) {
  	            return [
  	                '...',
  	                this.generateExpression(expr.argument, Precedence.Assignment, E_TTT)
  	            ];
  	        },

  	        TaggedTemplateExpression: function (expr, precedence, flags) {
  	            var itemFlags = E_TTF;
  	            if (!(flags & F_ALLOW_CALL)) {
  	                itemFlags = E_TFF;
  	            }
  	            var result = [
  	                this.generateExpression(expr.tag, Precedence.Call, itemFlags),
  	                this.generateExpression(expr.quasi, Precedence.Primary, E_FFT)
  	            ];
  	            return parenthesize(result, Precedence.TaggedTemplate, precedence);
  	        },

  	        TemplateElement: function (expr, precedence, flags) {
  	            // Don't use "cooked". Since tagged template can use raw template
  	            // representation. So if we do so, it breaks the script semantics.
  	            return expr.value.raw;
  	        },

  	        TemplateLiteral: function (expr, precedence, flags) {
  	            var result, i, iz;
  	            result = [ '`' ];
  	            for (i = 0, iz = expr.quasis.length; i < iz; ++i) {
  	                result.push(this.generateExpression(expr.quasis[i], Precedence.Primary, E_TTT));
  	                if (i + 1 < iz) {
  	                    result.push('${' + space);
  	                    result.push(this.generateExpression(expr.expressions[i], Precedence.Sequence, E_TTT));
  	                    result.push(space + '}');
  	                }
  	            }
  	            result.push('`');
  	            return result;
  	        },

  	        ModuleSpecifier: function (expr, precedence, flags) {
  	            return this.Literal(expr, precedence, flags);
  	        },

  	        ImportExpression: function(expr, precedence, flag) {
  	            return parenthesize([
  	                'import(',
  	                this.generateExpression(expr.source, Precedence.Assignment, E_TTT),
  	                ')'
  	            ], Precedence.Call, precedence);
  	        }
  	    };

  	    merge(CodeGenerator.prototype, CodeGenerator.Expression);

  	    CodeGenerator.prototype.generateExpression = function (expr, precedence, flags) {
  	        var result, type;

  	        type = expr.type || Syntax.Property;

  	        if (extra.verbatim && expr.hasOwnProperty(extra.verbatim)) {
  	            return generateVerbatim(expr, precedence);
  	        }

  	        result = this[type](expr, precedence, flags);


  	        if (extra.comment) {
  	            result = addComments(expr, result);
  	        }
  	        return toSourceNodeWhenNeeded(result, expr);
  	    };

  	    CodeGenerator.prototype.generateStatement = function (stmt, flags) {
  	        var result,
  	            fragment;

  	        result = this[stmt.type](stmt, flags);

  	        // Attach comments

  	        if (extra.comment) {
  	            result = addComments(stmt, result);
  	        }

  	        fragment = toSourceNodeWhenNeeded(result).toString();
  	        if (stmt.type === Syntax.Program && !safeConcatenation && newline === '' &&  fragment.charAt(fragment.length - 1) === '\n') {
  	            result = sourceMap ? toSourceNodeWhenNeeded(result).replaceRight(/\s+$/, '') : fragment.replace(/\s+$/, '');
  	        }

  	        return toSourceNodeWhenNeeded(result, stmt);
  	    };

  	    function generateInternal(node) {
  	        var codegen;

  	        codegen = new CodeGenerator();
  	        if (isStatement(node)) {
  	            return codegen.generateStatement(node, S_TFFF);
  	        }

  	        if (isExpression(node)) {
  	            return codegen.generateExpression(node, Precedence.Sequence, E_TTT);
  	        }

  	        throw new Error('Unknown node type: ' + node.type);
  	    }

  	    function generate(node, options) {
  	        var defaultOptions = getDefaultOptions(), result, pair;

  	        if (options != null) {
  	            // Obsolete options
  	            //
  	            //   `options.indent`
  	            //   `options.base`
  	            //
  	            // Instead of them, we can use `option.format.indent`.
  	            if (typeof options.indent === 'string') {
  	                defaultOptions.format.indent.style = options.indent;
  	            }
  	            if (typeof options.base === 'number') {
  	                defaultOptions.format.indent.base = options.base;
  	            }
  	            options = updateDeeply(defaultOptions, options);
  	            indent = options.format.indent.style;
  	            if (typeof options.base === 'string') {
  	                base = options.base;
  	            } else {
  	                base = stringRepeat(indent, options.format.indent.base);
  	            }
  	        } else {
  	            options = defaultOptions;
  	            indent = options.format.indent.style;
  	            base = stringRepeat(indent, options.format.indent.base);
  	        }
  	        json = options.format.json;
  	        renumber = options.format.renumber;
  	        hexadecimal = json ? false : options.format.hexadecimal;
  	        quotes = json ? 'double' : options.format.quotes;
  	        escapeless = options.format.escapeless;
  	        newline = options.format.newline;
  	        space = options.format.space;
  	        if (options.format.compact) {
  	            newline = space = indent = base = '';
  	        }
  	        parentheses = options.format.parentheses;
  	        semicolons = options.format.semicolons;
  	        safeConcatenation = options.format.safeConcatenation;
  	        directive = options.directive;
  	        parse = json ? null : options.parse;
  	        sourceMap = options.sourceMap;
  	        sourceCode = options.sourceCode;
  	        preserveBlankLines = options.format.preserveBlankLines && sourceCode !== null;
  	        extra = options;

  	        if (sourceMap) {
  	            if (!exports$1.browser) {
  	                // We assume environment is node.js
  	                // And prevent from including source-map by browserify
  	                SourceNode = requireSourceMap().SourceNode;
  	            } else {
  	                SourceNode = commonjsGlobal.sourceMap.SourceNode;
  	            }
  	        }

  	        result = generateInternal(node);

  	        if (!sourceMap) {
  	            pair = {code: result.toString(), map: null};
  	            return options.sourceMapWithCode ? pair : pair.code;
  	        }


  	        pair = result.toStringWithSourceMap({
  	            file: options.file,
  	            sourceRoot: options.sourceMapRoot
  	        });

  	        if (options.sourceContent) {
  	            pair.map.setSourceContent(options.sourceMap,
  	                                      options.sourceContent);
  	        }

  	        if (options.sourceMapWithCode) {
  	            return pair;
  	        }

  	        return pair.map.toString();
  	    }

  	    FORMAT_MINIFY = {
  	        indent: {
  	            style: '',
  	            base: 0
  	        },
  	        renumber: true,
  	        hexadecimal: true,
  	        quotes: 'auto',
  	        escapeless: true,
  	        compact: true,
  	        parentheses: false,
  	        semicolons: false
  	    };

  	    FORMAT_DEFAULTS = getDefaultOptions().format;

  	    exports$1.version = require$$3.version;
  	    exports$1.generate = generate;
  	    exports$1.attachComments = estraverse$1.attachComments;
  	    exports$1.Precedence = updateDeeply({}, Precedence);
  	    exports$1.browser = false;
  	    exports$1.FORMAT_MINIFY = FORMAT_MINIFY;
  	    exports$1.FORMAT_DEFAULTS = FORMAT_DEFAULTS;
  	}());
  	/* vim: set sw=4 ts=4 et tw=80 : */ 
  } (escodegen$1));

  var escodegen = /*@__PURE__*/getDefaultExportFromCjs(escodegen$1);

  let blockVarCounter = 0;
  let loopVarCounter = 0;
  function replaceBinaryOperator(codeSource) {
    switch (codeSource) {
      case '+': return 'add';
      case '-': return 'sub';
      case '*': return 'mult';
      case '/': return 'div';
      case '%': return 'mod';
      case '==':
      case '===': return 'equalTo';
      case '!=':
      case '!==': return 'notEqual';
      case '>': return 'greaterThan';
      case '>=': return 'greaterEqual';
      case '<': return 'lessThan';
      case '<=': return 'lessEqual';
      case '&&': return 'and';
      case '||': return 'or';
      // TODO: handle ** --> pow, but make it stay pow in
      // GLSL instead of turning it back into **
    }
  }
  function nodeIsUniform(ancestor) {
    return ancestor.type === 'CallExpression'
      && (
        (
          // Global mode
          ancestor.callee?.type === 'Identifier' &&
          ancestor.callee?.name.startsWith('uniform')
        ) || (
          // Instance mode
          ancestor.callee?.type === 'MemberExpression' &&
          ancestor.callee?.property.name.startsWith('uniform')
        )
      );
  }

  function nodeIsVarying(node) {
    return node?.type === 'CallExpression'
      && (
        (
          // Global mode
          node.callee?.type === 'Identifier' &&
          (node.callee?.name.startsWith('varying') || node.callee?.name.startsWith('shared'))
        ) || (
          // Instance mode
          node.callee?.type === 'MemberExpression' &&
          (node.callee?.property.name.startsWith('varying') || node.callee?.property.name.startsWith('shared'))
        )
      );
  }

  // Helper function to check if a statement is a variable declaration with strands control flow init
  function statementContainsStrandsControlFlow(stmt) {
    // Check for variable declarations with strands control flow init
    if (stmt.type === 'VariableDeclaration') {
      const match = stmt.declarations.some(decl =>
        decl.init?.type === 'CallExpression' &&
        (
          (
            decl.init?.callee?.type === 'MemberExpression' &&
            decl.init?.callee?.object?.type === 'Identifier' &&
            decl.init?.callee?.object?.name === '__p5' &&
            (decl.init?.callee?.property?.name === 'strandsFor' ||
              decl.init?.callee?.property?.name === 'strandsIf')
          ) ||
          (
            decl.init?.callee?.type === 'Identifier' &&
            (decl.init?.callee?.name === '__p5.strandsFor' ||
              decl.init?.callee?.name === '__p5.strandsIf')
          )
        )
      );
      return match
    }
    return false;
  }

  // Helper function to build property path from MemberExpression
  // e.g., inputs.color -> "inputs.color", vec.x -> "vec.x"
  function isSwizzle(propertyName) {
    if (!propertyName || typeof propertyName !== 'string') return false;
    const swizzleSets = [
      ['x', 'y', 'z', 'w'],
      ['r', 'g', 'b', 'a'],
      ['s', 't', 'p', 'q']
    ];
    return swizzleSets.some(set =>
      [...propertyName].every(char => set.includes(char))
    );
  }

  function buildPropertyPath(memberExpr) {
    const parts = [];
    let current = memberExpr;
    while (current.type === 'MemberExpression') {
      if (current.computed) {
        return null;
      }
      const propName = current.property.name || current.property.value;
      if (isSwizzle(propName)) {
        current = current.object;
        break;
      }
      parts.unshift(propName);
      current = current.object;
    }
    if (current.type === 'Identifier') {
      parts.unshift(current.name);
    } else {
      return null;
    }
    return parts.join('.');
  }

  // Replace all references to original variables with temp variables
  // and wrap literal assignments in strandsNode calls
  function replaceReferences(node, tempVarMap) {
    const internalReplaceReferences = (node) => {
      if (!node || typeof node !== 'object') return;

      // Check if this MemberExpression matches a tracked property path
      if (node.type === 'MemberExpression') {
        const propName = node.property.name || node.property.value;
        if (isSwizzle(propName)) {
          // For swizzles, only replace the object part, keep the swizzle
          internalReplaceReferences(node.object);
          return;
        }
        const propertyPath = buildPropertyPath(node);
        if (propertyPath && tempVarMap.has(propertyPath)) {
          // Replace entire member expression with temp variable
          Object.assign(node, {
            type: 'Identifier',
            name: tempVarMap.get(propertyPath)
          });
          return; // Don't recurse into replaced node
        }
      }

      // Handle simple identifier replacements
      if (node.type === 'Identifier' && tempVarMap.has(node.name)) {
        node.name = tempVarMap.get(node.name);
      }

      // Handle literal assignments to temp variables
      if (node.type === 'AssignmentExpression') {
        let leftPath = null;
        if (node.left.type === 'Identifier') {
          leftPath = node.left.name;
        } else if (node.left.type === 'MemberExpression') {
          leftPath = buildPropertyPath(node.left);
        }

        if (leftPath && tempVarMap.has(leftPath) &&
            (node.right.type === 'Literal' || node.right.type === 'ArrayExpression')) {
          // Wrap the right hand side in a strandsNode call to make sure
          // it's not just a literal and has a type
          node.right = {
            type: 'CallExpression',
            callee: {
              type: 'Identifier',
              name: '__p5.strandsNode'
            },
            arguments: [node.right]
          };
        }
      }

      // Recursively process all properties
      for (const key in node) {
        if (node.hasOwnProperty(key) && key !== 'parent') {
          // Don't recurse into property names of non-computed member expressions
          if (node.type === 'MemberExpression' && key === 'property' && !node.computed) {
            continue;
          }
          if (Array.isArray(node[key])) {
            node[key].forEach(internalReplaceReferences);
          } else if (typeof node[key] === 'object') {
            internalReplaceReferences(node[key]);
          }
        }
      }
    };

    internalReplaceReferences(node);
  }

  const ASTCallbacks = {
    UnaryExpression(node, _state, ancestors) {
      if (ancestors.some(nodeIsUniform)) { return; }
      const unaryFnName = UnarySymbolToName[node.operator];
      const standardReplacement = (node) => {
        node.type = 'CallExpression';
        node.callee = {
          type: 'Identifier',
          name: `__p5.${unaryFnName}`,
        };
        node.arguments = [node.argument];
      };
      if (node.type === 'MemberExpression') {
        const property = node.argument.property.name;
        const swizzleSets = [
          ['x', 'y', 'z', 'w'],
          ['r', 'g', 'b', 'a'],
          ['s', 't', 'p', 'q']
        ];
        let isSwizzle = swizzleSets.some(set =>
          [...property].every(char => set.includes(char))
        ) && node.argument.type === 'MemberExpression';
        if (isSwizzle) {
          node.type = 'MemberExpression';
          node.object = {
            type: 'CallExpression',
            callee: {
              type: 'Identifier',
              name: `__p5.${unaryFnName}`
            },
            arguments: [node.argument.object],
          };
          node.property = {
            type: 'Identifier',
            name: property
          };
        } else {
          standardReplacement(node);
        }
      } else {
        standardReplacement(node);
      }
      delete node.argument;
      delete node.operator;
    },
    BreakStatement(node, _state, ancestors) {
      if (ancestors.some(nodeIsUniform)) { return; }
      node.callee = {
        type: 'Identifier',
        name: '__p5.break'
      };
      node.arguments = [];
      node.type = 'CallExpression';
    },
    VariableDeclarator(node, _state, ancestors) {
      if (ancestors.some(nodeIsUniform)) { return; }
      if (nodeIsUniform(node.init)) {
        // Only inject the variable name if the first argument isn't already a string
        if (node.init.arguments.length === 0 ||
            node.init.arguments[0].type !== 'Literal' ||
            typeof node.init.arguments[0].value !== 'string') {
          const uniformNameLiteral = {
            type: 'Literal',
            value: node.id.name
          };
          node.init.arguments.unshift(uniformNameLiteral);
        }
      }
      if (nodeIsVarying(node.init)) {
        // Only inject the variable name if the first argument isn't already a string
        if (
          node.init.arguments.length === 0 ||
          node.init.arguments[0].type !== 'Literal' ||
          typeof node.init.arguments[0].value !== 'string'
        ) {
          const varyingNameLiteral = {
            type: 'Literal',
            value: node.id.name
          };
          node.init.arguments.unshift(varyingNameLiteral);
          _state.varyings[node.id.name] = varyingNameLiteral;
        } else {
          // Still track it as a varying even if name wasn't injected
          _state.varyings[node.id.name] = node.init.arguments[0];
        }
      }
    },
    Identifier(node, _state, ancestors) {
      if (ancestors.some(nodeIsUniform)) { return; }
      if (_state.varyings[node.name]
        && !ancestors.some(a => a.type === 'AssignmentExpression' && a.left === node)) {
          node.type = 'CallExpression';
          node.callee = {
            type: 'MemberExpression',
            object: {
              type: 'Identifier',
              name: node.name
            },
            property: {
              type: 'Identifier',
              name: 'getValue'
            },
          };
          node.arguments = [];
        }
      },
      // The callbacks for AssignmentExpression and BinaryExpression handle
      // operator overloading including +=, *= assignment expressions
      ArrayExpression(node, _state, ancestors) {
        if (ancestors.some(nodeIsUniform)) { return; }
        const original = JSON.parse(JSON.stringify(node));
        node.type = 'CallExpression';
        node.callee = {
          type: 'Identifier',
          name: '__p5.strandsNode',
        };
        node.arguments = [original];
      },
      AssignmentExpression(node, _state, ancestors) {
        if (ancestors.some(nodeIsUniform)) { return; }
        const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
        if (node.operator !== '=') {
          const methodName = replaceBinaryOperator(node.operator.replace('=',''));
          const rightReplacementNode = {
            type: 'CallExpression',
            callee: {
              type: 'MemberExpression',
              object: unsafeTypes.includes(node.left.type)
                ? {
                    type: 'CallExpression',
                    callee: {
                      type: 'Identifier',
                      name: '__p5.strandsNode',
                    },
                    arguments: [node.left]
                  }
                : node.left,
              property: {
                type: 'Identifier',
                name: methodName,
              },
            },
            arguments: [node.right]
          };
          node.operator = '=';
          node.right = rightReplacementNode;
        }
        // Handle direct varying variable assignment: myVarying = value
        if (_state.varyings[node.left.name]) {
          node.type = 'ExpressionStatement';
          node.expression = {
            type: 'CallExpression',
            callee: {
              type: 'MemberExpression',
              object: {
                type: 'Identifier',
                name: node.left.name
              },
              property: {
                type: 'Identifier',
                name: 'bridge',
              }
            },
            arguments: [node.right],
          };
        }
        // Handle swizzle assignment to varying variable: myVarying.xyz = value
        // Note: node.left.object might be worldPos.getValue() due to prior Identifier transformation
        else if (node.left.type === 'MemberExpression') {
          let varyingName = null;

          // Check if it's a direct identifier: myVarying.xyz
          if (node.left.object.type === 'Identifier' && _state.varyings[node.left.object.name]) {
            varyingName = node.left.object.name;
          }
          // Check if it's a getValue() call: myVarying.getValue().xyz
          else if (node.left.object.type === 'CallExpression' &&
                   node.left.object.callee?.type === 'MemberExpression' &&
                   node.left.object.callee.property?.name === 'getValue' &&
                   node.left.object.callee.object?.type === 'Identifier' &&
                   _state.varyings[node.left.object.callee.object.name]) {
            varyingName = node.left.object.callee.object.name;
          }

          if (varyingName) {
            const swizzlePattern = node.left.property.name;
            node.type = 'ExpressionStatement';
            node.expression = {
              type: 'CallExpression',
              callee: {
                type: 'MemberExpression',
                object: {
                  type: 'Identifier',
                  name: varyingName
                },
                property: {
                  type: 'Identifier',
                  name: 'bridgeSwizzle',
                }
              },
              arguments: [
                {
                  type: 'Literal',
                  value: swizzlePattern
                },
                node.right
              ],
            };
          }
        }
      },
      BinaryExpression(node, _state, ancestors) {
        // Don't convert uniform default values to node methods, as
        // they should be evaluated at runtime, not compiled.
        if (ancestors.some(nodeIsUniform)) { return; }
        // If the left hand side of an expression is one of these types,
        // we should construct a node from it.
        const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
        if (unsafeTypes.includes(node.left.type)) {
          const leftReplacementNode = {
            type: 'CallExpression',
            callee: {
              type: 'Identifier',
              name: '__p5.strandsNode',
            },
            arguments: [node.left]
          };
          node.left = leftReplacementNode;
        }
        // Replace the binary operator with a call expression
        // in other words a call to BaseNode.mult(), .div() etc.
        node.type = 'CallExpression';
        node.callee = {
          type: 'MemberExpression',
          object: node.left,
          property: {
            type: 'Identifier',
            name: replaceBinaryOperator(node.operator),
          },
        };
        node.arguments = [node.right];
      },
      LogicalExpression(node, _state, ancestors) {
        // Don't convert uniform default values to node methods, as
        // they should be evaluated at runtime, not compiled.
        if (ancestors.some(nodeIsUniform)) { return; }
        // If the left hand side of an expression is one of these types,
        // we should construct a node from it.
        const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
        if (unsafeTypes.includes(node.left.type)) {
          const leftReplacementNode = {
            type: 'CallExpression',
            callee: {
              type: 'Identifier',
              name: '__p5.strandsNode',
            },
            arguments: [node.left]
          };
          node.left = leftReplacementNode;
        }
        // Replace the logical operator with a call expression
        // in other words a call to BaseNode.or(), .and() etc.
        node.type = 'CallExpression';
        node.callee = {
          type: 'MemberExpression',
          object: node.left,
          property: {
            type: 'Identifier',
            name: replaceBinaryOperator(node.operator),
          },
        };
        node.arguments = [node.right];
      },
      IfStatement(node, _state, ancestors) {
        if (ancestors.some(nodeIsUniform)) { return; }
        // Transform if statement into strandsIf() call
        // The condition is evaluated directly, not wrapped in a function
        const condition = node.test;
        // Create the then function
        const thenFunction = {
          type: 'ArrowFunctionExpression',
          params: [],
          body: node.consequent.type === 'BlockStatement' ? node.consequent : {
            type: 'BlockStatement',
            body: [node.consequent]
          }
        };
        // Start building the call chain: __p5.strandsIf(condition, then)
        let callExpression = {
          type: 'CallExpression',
          callee: {
            type: 'Identifier',
            name: '__p5.strandsIf'
          },
          arguments: [condition, thenFunction]
        };
        // Always chain .Else() even if there's no explicit else clause
        // This ensures the conditional completes and returns phi nodes
        let elseFunction;
        if (node.alternate) {
          elseFunction = {
            type: 'ArrowFunctionExpression',
            params: [],
            body: node.alternate.type === 'BlockStatement' ? node.alternate : {
              type: 'BlockStatement',
              body: [node.alternate]
            }
          };
        } else {
          // Create an empty else function
          elseFunction = {
            type: 'ArrowFunctionExpression',
            params: [],
            body: {
              type: 'BlockStatement',
              body: []
            }
          };
        }
        callExpression = {
          type: 'CallExpression',
          callee: {
            type: 'MemberExpression',
            object: callExpression,
            property: {
              type: 'Identifier',
              name: 'Else'
            }
          },
          arguments: [elseFunction]
        };

        // Analyze which outer scope variables are assigned in any branch
        const assignedVars = new Set();

        const analyzeBranch = (functionBody) => {
          // First pass: collect all variable declarations in the branch
          const localVars = new Set();
          ancestor(functionBody, {
            VariableDeclarator(node, ancestors) {
              // Skip if we're inside a block that contains strands control flow
              if (ancestors.some(statementContainsStrandsControlFlow)) return;
              if (node.id.type === 'Identifier') {
                localVars.add(node.id.name);
              }
            }
          });

          // Second pass: find assignments to non-local variables using acorn-walk
          ancestor(functionBody, {
            AssignmentExpression(node, ancestors) {
              // Skip if we're inside a block that contains strands control flow
              if (ancestors.some(statementContainsStrandsControlFlow)) return;

              const left = node.left;
              if (left.type === 'Identifier') {
                // Direct variable assignment: x = value
                if (!localVars.has(left.name)) {
                  assignedVars.add(left.name);
                }
              } else if (left.type === 'MemberExpression') {
                // Property assignment: obj.prop = value or obj.a.b = value
                const propertyPath = buildPropertyPath(left);
                if (propertyPath) {
                  const baseName = propertyPath.split('.')[0];
                  if (!localVars.has(baseName)) {
                    assignedVars.add(propertyPath);
                  }
                }
              }
            }
          });
        };

        // Analyze all branches for assignments to outer scope variables
        analyzeBranch(thenFunction.body);
        analyzeBranch(elseFunction.body);
        if (assignedVars.size > 0) {
          // Add copying, reference replacement, and return statements to branch functions
          const addCopyingAndReturn = (functionBody, varsToReturn) => {
            if (functionBody.type === 'BlockStatement') {
              // Create temporary variables and copy statements
              const tempVarMap = new Map(); // property path -> temp name
              const copyStatements = [];
              for (const varPath of varsToReturn) {
                const parts = varPath.split('.');
                const tempName = `__copy_${parts.join('_')}_${blockVarCounter++}`;
                tempVarMap.set(varPath, tempName);

                // Build the member expression for the property path
                let sourceExpr = { type: 'Identifier', name: parts[0] };
                for (let i = 1; i < parts.length; i++) {
                  sourceExpr = {
                    type: 'MemberExpression',
                    object: sourceExpr,
                    property: { type: 'Identifier', name: parts[i] },
                    computed: false
                  };
                }

                // let tempName = propertyPath.copy()
                copyStatements.push({
                  type: 'VariableDeclaration',
                  declarations: [{
                    type: 'VariableDeclarator',
                    id: { type: 'Identifier', name: tempName },
                    init: {
                      type: 'CallExpression',
                      callee: {
                        type: 'MemberExpression',
                        object: sourceExpr,
                        property: { type: 'Identifier', name: 'copy' },
                        computed: false
                      },
                      arguments: []
                    }
                  }],
                  kind: 'let'
                });
              }
              // Apply reference replacement to all statements
              functionBody.body.forEach(node => replaceReferences(node, tempVarMap));
              // Insert copy statements at the beginning
              functionBody.body.unshift(...copyStatements);
              // Add return statement with flat object using property paths as keys
              const returnObj = {
                type: 'ObjectExpression',
                properties: Array.from(varsToReturn).map(varPath => ({
                  type: 'Property',
                  key: { type: 'Literal', value: varPath },
                  value: { type: 'Identifier', name: tempVarMap.get(varPath) },
                  kind: 'init',
                  computed: false,
                  shorthand: false
                }))
              };
              functionBody.body.push({
                type: 'ReturnStatement',
                argument: returnObj
              });
            }
          };
          addCopyingAndReturn(thenFunction.body, assignedVars);
          addCopyingAndReturn(elseFunction.body, assignedVars);
          // Create a block variable to capture the return value
          const blockVar = `__block_${blockVarCounter++}`;
          // Replace with a block statement
          const statements = [];
          // Make sure every assigned variable starts as a node
          for (const varPath of assignedVars) {
            const parts = varPath.split('.');

            // Build left side: inputs.color or just x
            let leftExpr = { type: 'Identifier', name: parts[0] };
            for (let i = 1; i < parts.length; i++) {
              leftExpr = {
                type: 'MemberExpression',
                object: leftExpr,
                property: { type: 'Identifier', name: parts[i] },
                computed: false
              };
            }

            // Build right side - same as left for strandsNode wrapping
            let rightArgExpr = { type: 'Identifier', name: parts[0] };
            for (let i = 1; i < parts.length; i++) {
              rightArgExpr = {
                type: 'MemberExpression',
                object: rightArgExpr,
                property: { type: 'Identifier', name: parts[i] },
                computed: false
              };
            }

            statements.push({
              type: 'ExpressionStatement',
              expression: {
                type: 'AssignmentExpression',
                operator: '=',
                left: leftExpr,
                right: {
                  type: 'CallExpression',
                  callee: { type: 'Identifier', name: '__p5.strandsNode' },
                  arguments: [rightArgExpr],
                }
              }
            });
          }
          statements.push({
            type: 'VariableDeclaration',
            declarations: [{
              type: 'VariableDeclarator',
              id: { type: 'Identifier', name: blockVar },
              init: callExpression
            }],
            kind: 'const'
          });
          // 2. Assignments for each modified variable
          for (const varPath of assignedVars) {
            const parts = varPath.split('.');

            // Build left side: inputs.color or just x
            let leftExpr = { type: 'Identifier', name: parts[0] };
            for (let i = 1; i < parts.length; i++) {
              leftExpr = {
                type: 'MemberExpression',
                object: leftExpr,
                property: { type: 'Identifier', name: parts[i] },
                computed: false
              };
            }

            // Build right side: __block_2['inputs.color'] or __block_2['x']
            const rightExpr = {
              type: 'MemberExpression',
              object: { type: 'Identifier', name: blockVar },
              property: { type: 'Literal', value: varPath },
              computed: true
            };

            statements.push({
              type: 'ExpressionStatement',
              expression: {
                type: 'AssignmentExpression',
                operator: '=',
                left: leftExpr,
                right: rightExpr
              }
            });
          }
          // Replace the if statement with a block statement
          node.type = 'BlockStatement';
          node.body = statements;
        } else {
          // No assignments, just replace with the call expression
          node.type = 'ExpressionStatement';
          node.expression = callExpression;
        }
        delete node.test;
        delete node.consequent;
        delete node.alternate;
      },
      UpdateExpression(node, _state, ancestors) {
        if (ancestors.some(nodeIsUniform)) { return; }

        // Transform ++var, var++, --var, var-- into assignment expressions
        let operator;
        if (node.operator === '++') {
          operator = '+';
        } else if (node.operator === '--') {
          operator = '-';
        } else {
          return; // Unknown update operator
        }

        // Convert to: var = var + 1 or var = var - 1
        const assignmentExpr = {
          type: 'AssignmentExpression',
          operator: '=',
          left: node.argument,
          right: {
            type: 'BinaryExpression',
            operator: operator,
            left: node.argument,
            right: {
              type: 'Literal',
              value: 1
            }
          }
        };

        // Replace the update expression with the assignment expression
        Object.assign(node, assignmentExpr);
        delete node.prefix;
        this.BinaryExpression(node.right, _state, [...ancestors, node]);
        this.AssignmentExpression(node, _state, ancestors);
      },
      ForStatement(node, _state, ancestors) {
        if (ancestors.some(nodeIsUniform)) { return; }

        // Transform for statement into strandsFor() call
        // for (init; test; update) body -> strandsFor(initCb, conditionCb, updateCb, bodyCb, initialVars)

        // Generate unique loop variable name
        const uniqueLoopVar = `loopVar${loopVarCounter++}`;

        // Create the initial callback from the for loop's init
        let initialFunction;
        if (node.init && node.init.type === 'VariableDeclaration') {
          // Handle: for (let i = 0; ...)
          const declaration = node.init.declarations[0];
          let initValue = declaration.init;

          const initAst = { body: [{ type: 'ExpressionStatement', expression: initValue }] };
          initValue = initAst.body[0].expression;

          initialFunction = {
            type: 'ArrowFunctionExpression',
            params: [],
            body: {
              type: 'BlockStatement',
              body: [{
                type: 'ReturnStatement',
                argument: initValue
              }]
            }
          };
        } else {
          // Handle other cases - return a default value
          initialFunction = {
            type: 'ArrowFunctionExpression',
            params: [],
            body: {
              type: 'BlockStatement',
              body: [{
                type: 'ReturnStatement',
                argument: {
                  type: 'Literal',
                  value: 0
                }
              }]
            }
          };
        }

        // Create the condition callback
        let conditionBody = node.test || { type: 'Literal', value: true };
        // Replace loop variable references with the parameter
        if (node.init?.type === 'VariableDeclaration') {
          const loopVarName = node.init.declarations[0].id.name;
          conditionBody = this.replaceIdentifierReferences(conditionBody, loopVarName, uniqueLoopVar);
        }
        const conditionAst = { body: [{ type: 'ExpressionStatement', expression: conditionBody }] };
        conditionBody = conditionAst.body[0].expression;

        const conditionFunction = {
          type: 'ArrowFunctionExpression',
          params: [{ type: 'Identifier', name: uniqueLoopVar }],
          body: conditionBody
        };

        // Create the update callback
        let updateFunction;
        if (node.update) {
          let updateExpr = node.update;
          // Replace loop variable references with the parameter
          if (node.init?.type === 'VariableDeclaration') {
            const loopVarName = node.init.declarations[0].id.name;
            updateExpr = this.replaceIdentifierReferences(updateExpr, loopVarName, uniqueLoopVar);
          }
          const updateAst = { body: [{ type: 'ExpressionStatement', expression: updateExpr }] };
          updateExpr = updateAst.body[0].expression;

          updateFunction = {
            type: 'ArrowFunctionExpression',
            params: [{ type: 'Identifier', name: uniqueLoopVar }],
            body: {
              type: 'BlockStatement',
              body: [{
                type: 'ReturnStatement',
                argument: updateExpr
              }]
            }
          };
        } else {
          updateFunction = {
            type: 'ArrowFunctionExpression',
            params: [{ type: 'Identifier', name: uniqueLoopVar }],
            body: {
              type: 'BlockStatement',
              body: [{
                type: 'ReturnStatement',
                argument: { type: 'Identifier', name: uniqueLoopVar }
              }]
            }
          };
        }

        // Create the body callback
        let bodyBlock = node.body.type === 'BlockStatement' ? node.body : {
          type: 'BlockStatement',
          body: [node.body]
        };

        // Replace loop variable references in the body
        if (node.init?.type === 'VariableDeclaration') {
          const loopVarName = node.init.declarations[0].id.name;
          bodyBlock = this.replaceIdentifierReferences(bodyBlock, loopVarName, uniqueLoopVar);
        }

        const bodyFunction = {
          type: 'ArrowFunctionExpression',
          params: [
            { type: 'Identifier', name: uniqueLoopVar },
            { type: 'Identifier', name: 'vars' }
          ],
          body: bodyBlock
        };

        // Analyze which outer scope variables are assigned in the loop body
        const assignedVars = new Set();

        // First pass: collect all variable declarations in the body
        const localVars = new Set();
        ancestor(bodyFunction.body, {
          VariableDeclarator(node, ancestors) {
            // Skip if we're inside a block that contains strands control flow
            if (ancestors.some(statementContainsStrandsControlFlow)) return;
            if (node.id.type === 'Identifier') {
              localVars.add(node.id.name);
            }
          }
        });

        // Second pass: find assignments to non-local variables using acorn-walk
        ancestor(bodyFunction.body, {
          AssignmentExpression(node, ancestors) {
            // Skip if we're inside a block that contains strands control flow
            if (ancestors.some(statementContainsStrandsControlFlow)) {
              return
            }

            const left = node.left;
            if (left.type === 'Identifier') {
              // Direct variable assignment: x = value
              if (!localVars.has(left.name)) {
                assignedVars.add(left.name);
              }
            } else if (left.type === 'MemberExpression') {
              // Property assignment: obj.prop = value or obj.a.b = value
              const propertyPath = buildPropertyPath(left);
              if (propertyPath) {
                const baseName = propertyPath.split('.')[0];
                if (!localVars.has(baseName)) {
                  assignedVars.add(propertyPath);
                }
              }
            }
          }
        });

        if (assignedVars.size > 0) {
          // Add copying, reference replacement, and return statements similar to if statements
          const addCopyingAndReturn = (functionBody, varsToReturn) => {
            if (functionBody.type === 'BlockStatement') {
              const tempVarMap = new Map();
              const copyStatements = [];

              for (const varPath of varsToReturn) {
                const parts = varPath.split('.');
                const tempName = `__copy_${parts.join('_')}_${blockVarCounter++}`;
                tempVarMap.set(varPath, tempName);

                // Build the member expression for vars.propertyPath
                // e.g., vars.inputs.color or vars.x
                let sourceExpr = { type: 'Identifier', name: 'vars' };
                for (const part of parts) {
                  sourceExpr = {
                    type: 'MemberExpression',
                    object: sourceExpr,
                    property: { type: 'Identifier', name: part },
                    computed: false
                  };
                }

                copyStatements.push({
                  type: 'VariableDeclaration',
                  declarations: [{
                    type: 'VariableDeclarator',
                    id: { type: 'Identifier', name: tempName },
                    init: {
                      type: 'CallExpression',
                      callee: {
                        type: 'MemberExpression',
                        object: sourceExpr,
                        property: { type: 'Identifier', name: 'copy' },
                        computed: false
                      },
                      arguments: []
                    }
                  }],
                  kind: 'let'
                });
              }

              functionBody.body.forEach(node => replaceReferences(node, tempVarMap));
              functionBody.body.unshift(...copyStatements);

              // Add return statement with flat object using property paths as keys
              const returnObj = {
                type: 'ObjectExpression',
                properties: Array.from(varsToReturn).map(varPath => ({
                  type: 'Property',
                  key: { type: 'Literal', value: varPath },
                  value: { type: 'Identifier', name: tempVarMap.get(varPath) },
                  kind: 'init',
                  computed: false,
                  shorthand: false
                }))
              };

              functionBody.body.push({
                type: 'ReturnStatement',
                argument: returnObj
              });
            }
          };

          addCopyingAndReturn(bodyFunction.body, assignedVars);

          // Create block variable and assignments similar to if statements
          const blockVar = `__block_${blockVarCounter++}`;
          const statements = [];

          const initialVarsObject = {
            type: 'ObjectExpression',
            properties: Array.from(assignedVars).map(varPath => {
              const parts = varPath.split('.');
              let expr = { type: 'Identifier', name: parts[0] };
              for (let i = 1; i < parts.length; i++) {
                expr = {
                  type: 'MemberExpression',
                  object: expr,
                  property: { type: 'Identifier', name: parts[i] },
                  computed: false
                };
              }
              const wrappedExpr = {
                type: 'CallExpression',
                callee: { type: 'Identifier', name: '__p5.strandsNode' },
                arguments: [expr]
              };
              return {
                type: 'Property',
                key: { type: 'Literal', value: varPath },
                value: wrappedExpr,
                kind: 'init',
                computed: false,
                shorthand: false
              };
            })
          };

          // Create the strandsFor call
          const callExpression = {
            type: 'CallExpression',
            callee: {
              type: 'Identifier',
              name: '__p5.strandsFor'
            },
            arguments: [initialFunction, conditionFunction, updateFunction, bodyFunction, initialVarsObject]
          };

          statements.push({
            type: 'VariableDeclaration',
            declarations: [{
              type: 'VariableDeclarator',
              id: { type: 'Identifier', name: blockVar },
              init: callExpression
            }],
            kind: 'const'
          });

          // Add assignments back to original variables
          for (const varPath of assignedVars) {
            const parts = varPath.split('.');

            // Build left side: inputs.color or just x
            let leftExpr = { type: 'Identifier', name: parts[0] };
            for (let i = 1; i < parts.length; i++) {
              leftExpr = {
                type: 'MemberExpression',
                object: leftExpr,
                property: { type: 'Identifier', name: parts[i] },
                computed: false
              };
            }

            // Build right side: __block_2.inputs.color or __block_2.x
            let rightExpr = { type: 'Identifier', name: blockVar };
            for (const part of parts) {
              rightExpr = {
                type: 'MemberExpression',
                object: rightExpr,
                property: { type: 'Identifier', name: part },
                computed: false
              };
            }

            statements.push({
              type: 'ExpressionStatement',
              expression: {
                type: 'AssignmentExpression',
                operator: '=',
                left: leftExpr,
                right: rightExpr
              }
            });
          }

          node.type = 'BlockStatement';
          node.body = statements;
        } else {
          // No assignments, just replace with call expression
          node.type = 'ExpressionStatement';
          node.expression = {
            type: 'CallExpression',
            callee: {
              type: 'Identifier',
              name: '__p5.strandsFor'
            },
            arguments: [initialFunction, conditionFunction, updateFunction, bodyFunction, {
              type: 'ObjectExpression',
              properties: []
            }]
          };
        }

        delete node.init;
        delete node.test;
        delete node.update;
      },

      // Helper method to replace identifier references in AST nodes
      replaceIdentifierReferences(node, oldName, newName) {
        if (!node || typeof node !== 'object') return node;

        const replaceInNode = (n) => {
          if (!n || typeof n !== 'object') return n;

          if (n.type === 'Identifier' && n.name === oldName) {
            return { ...n, name: newName };
          }

          // Create a copy and recursively process properties
          const newNode = { ...n };
          for (const key in n) {
            if (n.hasOwnProperty(key) && key !== 'parent') {
              if (Array.isArray(n[key])) {
                newNode[key] = n[key].map(replaceInNode);
              } else if (typeof n[key] === 'object') {
                newNode[key] = replaceInNode(n[key]);
              }
            }
          }
          return newNode;
        };

        return replaceInNode(node);
      }
    };
    function transpileStrandsToJS(p5, sourceString, srcLocations, scope) {
      // Reset counters at the start of each transpilation
      blockVarCounter = 0;
      loopVarCounter = 0;

      const ast = parse(sourceString, {
        ecmaVersion: 2021,
        locations: srcLocations
      });
      // First pass: transform everything except if/for statements using normal ancestor traversal
      const nonControlFlowCallbacks = { ...ASTCallbacks };
      delete nonControlFlowCallbacks.IfStatement;
      delete nonControlFlowCallbacks.ForStatement;
      ancestor(ast, nonControlFlowCallbacks, undefined, { varyings: {} });
      // Second pass: transform if/for statements in post-order using recursive traversal
      const postOrderControlFlowTransform = {
        IfStatement(node, state, c) {
          state.inControlFlow++;
          // First recursively process children
          if (node.test) c(node.test, state);
          if (node.consequent) c(node.consequent, state);
          if (node.alternate) c(node.alternate, state);
          // Then apply the transformation to this node
          ASTCallbacks.IfStatement(node, state, []);
          state.inControlFlow--;
        },
        ForStatement(node, state, c) {
          state.inControlFlow++;
          // First recursively process children
          if (node.init) c(node.init, state);
          if (node.test) c(node.test, state);
          if (node.update) c(node.update, state);
          if (node.body) c(node.body, state);
          // Then apply the transformation to this node
          ASTCallbacks.ForStatement(node, state, []);
          state.inControlFlow--;
        },
        ReturnStatement(node, state, c) {
          if (!state.inControlFlow) return;
          // Convert return statement to strandsEarlyReturn call
          node.type = 'ExpressionStatement';
          node.expression = {
            type: 'CallExpression',
            callee: {
              type: 'Identifier',
              name: '__p5.strandsEarlyReturn'
            },
            arguments: node.argument ? [node.argument] : []
          };
          delete node.argument;
        }
      };
      recursive(ast, { varyings: {}, inControlFlow: 0 }, postOrderControlFlowTransform);
      const transpiledSource = escodegen.generate(ast);
      const scopeKeys = Object.keys(scope);
      const match = /\(?\s*(?:function)?\s*\w*\s*\(([^)]*)\)\s*(?:=>)?\s*{((?:.|\n)*)}\s*;?\s*\)?/
        .exec(transpiledSource);
      if (!match) {
        console.log(transpiledSource);
        throw new Error('Could not parse p5.strands function!');
      }
      const params = match[1].split(/,\s*/).filter(param => !!param.trim());
      let paramVals, paramNames;
      if (params.length > 0) {
        paramNames = params;
        paramVals = [scope];
      } else {
        paramNames = scopeKeys;
        paramVals = scopeKeys.map(key => scope[key]);
      }
      const body = match[2];
      try {
        const internalStrandsCallback = new Function(
            // Create a parameter called __p5, not just p5, because users of instance mode
            // may pass in a variable called p5 as a scope variable. If we rely on a variable called
            // p5, then the scope variable called p5 might accidentally override internal function
            // calls to p5 static methods.
          '__p5',
          ...paramNames,
          body,
        );
        return () => internalStrandsCallback(p5, ...paramVals);
      } catch (e) {
        console.error(e);
        console.log(paramNames);
        console.log(body);
        throw new Error('Error transpiling p5.strands callback!');
      }
    }

  function generateShaderCode(strandsContext) {
    const {
      cfg,
      backend,
      vertexDeclarations,
      fragmentDeclarations
    } = strandsContext;

    const hooksObj = {
      uniforms: {},
      varyingVariables: [],
    };

    for (const {name, typeInfo, defaultValue} of strandsContext.uniforms) {
      const key = backend.generateHookUniformKey(name, typeInfo);
      if (key !== null) {
        hooksObj.uniforms[key] = defaultValue;
      }
    }

    // Add texture bindings to declarations for WebGPU backend
    if (backend.addTextureBindingsToDeclarations) {
      backend.addTextureBindingsToDeclarations(strandsContext);
    }

    for (const { hookType, rootNodeID, entryBlockID, shaderContext } of strandsContext.hooks) {
      const generationContext = {
        indent: 1,
        codeLines: [],
        write(line) {
          this.codeLines.push('  '.repeat(this.indent) + line);
        },
        tempNames: {},
        declarations: [],
        nextTempID: 0,
        visitedNodes: new Set(),
        shaderContext, // 'vertex' or 'fragment'
        strandsContext, // For shared variable tracking
      };

      const blocks = sortCFG(cfg.outgoingEdges, entryBlockID);
      for (const blockID of blocks) {
        backend.generateBlock(blockID, strandsContext, generationContext);
      }

      // Process any unvisited global assignments to ensure side effects are generated
      for (const assignmentNodeID of strandsContext.globalAssignments) {
        if (!generationContext.visitedNodes.has(assignmentNodeID)) {
          // This assignment hasn't been visited yet, so we need to generate it
          backend.generateAssignment(generationContext, strandsContext.dag, assignmentNodeID);
          generationContext.visitedNodes.add(assignmentNodeID);
        }
      }

      // Reset global assignments for next hook
      strandsContext.globalAssignments = [];

      const firstLine = backend.hookEntry(hookType);
      let returnType;
      if (hookType.returnType.properties) {
        returnType = structType(hookType.returnType);
      } else {
        if (!hookType.returnType.dataType) {
          throw new Error(`Missing dataType for return type ${hookType.returnType.typeName}`);
        }
        returnType = hookType.returnType.dataType;
      }

      if (rootNodeID) {
        backend.generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType);
      }
      hooksObj[`${hookType.returnType.typeName} ${hookType.name}`] = [firstLine, ...generationContext.codeLines, '}'].join('\n');
    }

    // Finalize shared variable declarations based on usage
    if (strandsContext.sharedVariables) {
      for (const [varName, varInfo] of strandsContext.sharedVariables) {
        if (varInfo.usedInVertex && varInfo.usedInFragment) {
          // Used in both shaders - this is a true varying variable
          hooksObj.varyingVariables.push(backend.generateVaryingVariable(varName, varInfo.typeInfo));
        } else if (varInfo.usedInVertex) {
          // Only used in vertex shader - declare as local variable
          vertexDeclarations.add(backend.generateLocalDeclaration(varName, varInfo.typeInfo));
        } else if (varInfo.usedInFragment) {
          // Only used in fragment shader - declare as local variable
          fragmentDeclarations.add(backend.generateLocalDeclaration(varName, varInfo.typeInfo));
        }
        // If not used anywhere, don't declare it
      }
    }

    hooksObj.vertexDeclarations = [...vertexDeclarations].join('\n');
    hooksObj.fragmentDeclarations = [...fragmentDeclarations].join('\n');

    return hooksObj;
  }

  function createPhiNode(strandsContext, phiInputs, varName) {
    // Determine the proper dimension and baseType from the inputs
    const validInputs = phiInputs.filter(input => input.value.id !== null);
    if (validInputs.length === 0) {
      throw new Error(`No valid inputs for phi node for variable ${varName}`);
    }
    // Get dimension and baseType from first valid input
    let firstInput = validInputs
      .map((input) => getNodeDataFromID(strandsContext.dag, input.value.id))
      .find((input) => input.dimension) ??
        getNodeDataFromID(strandsContext.dag, validInputs[0].value.id);
    const dimension = firstInput.dimension;
    const baseType = firstInput.baseType;
    const nodeData = {
      nodeType: NodeType.PHI,
      dimension,
      baseType,
      dependsOn: phiInputs.map(input => input.value.id).filter(id => id !== null),
      phiBlocks: phiInputs.map(input => input.blockId)};
    const id = getOrCreateNode(strandsContext.dag, nodeData);
    recordInBasicBlock(strandsContext.cfg, strandsContext.cfg.currentBlock, id);
    return {
      id,
      dimension,
      baseType
    };
  }

  class StrandsConditional {
    constructor(strandsContext, condition, branchCallback) {
      // Condition must be a node...
      this.branches = [{
        condition,
        branchCallback,
        blockType: BlockType.IF_BODY
      }];
      this.ctx = strandsContext;
    }
    ElseIf(condition, branchCallback) {
      this.branches.push({
        condition,
        branchCallback,
        blockType: BlockType.IF_BODY
      });
      return this;
    }
    Else(branchCallback = () => ({})) {
      this.branches.push({
        condition: null,
        branchCallback,
        blockType: BlockType.IF_BODY
      });
      const phiNodes = buildConditional(this.ctx, this);
      const assignments = {};
      for (const [varName, phiNode] of Object.entries(phiNodes)) {
        assignments[varName] = createStrandsNode(phiNode.id, phiNode.dimension, this.ctx);
      }
      return assignments;
    }
  }
  function buildConditional(strandsContext, conditional) {
    const cfg = strandsContext.cfg;
    const branches = conditional.branches;
    const mergeBlock = createBasicBlock(cfg, BlockType.MERGE);
    const results = [];
    const branchEndBlocks = [];
    const mergedAssignments = {};
    const phiBlockDependencies = {};
    // Create a BRANCH block to handle phi node declarations
    const branchBlock = createBasicBlock(cfg, BlockType.BRANCH);
    addEdge(cfg, cfg.currentBlock, branchBlock);
    addEdge(cfg, branchBlock, mergeBlock);
    let previousBlock = branchBlock;
    for (let i = 0; i < branches.length; i++) {
      const { condition, branchCallback, blockType } = branches[i];
      if (condition !== null) {
        const conditionBlock = createBasicBlock(cfg, BlockType.IF_COND);
        addEdge(cfg, previousBlock, conditionBlock);
        pushBlock(cfg, conditionBlock);
        cfg.blockConditions[conditionBlock] = condition.id;
        previousBlock = conditionBlock;
        popBlock(cfg);
      } else {
        const elseCondBlock = createBasicBlock(cfg, BlockType.ELSE_COND);
        addEdge(cfg, previousBlock, elseCondBlock);
        previousBlock = elseCondBlock;
      }
      const scopeStartBlock = createBasicBlock(cfg, BlockType.SCOPE_START);
      addEdge(cfg, previousBlock, scopeStartBlock);
      const branchContentBlock = createBasicBlock(cfg, blockType);
      addEdge(cfg, scopeStartBlock, branchContentBlock);
      pushBlock(cfg, branchContentBlock);
      const branchResults = branchCallback();
      for (const key in branchResults) {
        branchResults[key] = strandsContext.p5.strandsNode(branchResults[key]);
      }
      for (const key in branchResults) {
        if (!phiBlockDependencies[key]) {
          phiBlockDependencies[key] = [{ value: branchResults[key], blockId: branchContentBlock }];
        } else {
          phiBlockDependencies[key].push({ value: branchResults[key], blockId: branchContentBlock });
        }
      }
      results.push(branchResults);

      // Create BRANCH_END block for phi assignments
      const branchEndBlock = createBasicBlock(cfg, BlockType.DEFAULT);
      addEdge(cfg, cfg.currentBlock, branchEndBlock);
      branchEndBlocks.push(branchEndBlock);
      popBlock(cfg);

      const scopeEndBlock = createBasicBlock(cfg, BlockType.SCOPE_END);
      addEdge(cfg, branchEndBlock, scopeEndBlock);
      addEdge(cfg, scopeEndBlock, mergeBlock);
      previousBlock = scopeStartBlock;
    }
    // Push the branch block for modification to avoid changing the ordering
    pushBlockForModification(cfg, branchBlock);
    for (const key in phiBlockDependencies) {
      mergedAssignments[key] = createPhiNode(strandsContext, phiBlockDependencies[key], key);
    }
    popBlock(cfg);
    for (let i = 0; i < results.length; i++) {
      const branchResult = results[i];
      const branchEndBlockID = branchEndBlocks[i];
      pushBlockForModification(cfg, branchEndBlockID);
      for (const key in branchResult) {
        if (mergedAssignments[key]) {
          // Create an assignment statement: phiNode = branchResult[key]
          const phiNodeID = mergedAssignments[key].id;
          const sourceNodeID = branchResult[key].id;
          // Create an assignment operation node
          // Use dependsOn[0] for phiNodeID and dependsOn[1] for sourceNodeID
          // This represents: dependsOn[0] = dependsOn[1] (phiNode = sourceNode)
          const assignmentNode = {
            nodeType: NodeType.ASSIGNMENT,
            dependsOn: [phiNodeID, sourceNodeID],
            phiBlocks: []
          };
          const assignmentID = getOrCreateNode(strandsContext.dag, assignmentNode);
          recordInBasicBlock(cfg, branchEndBlockID, assignmentID);
        }
      }
      popBlock(cfg);
    }
    pushBlock(cfg, mergeBlock);
    return mergedAssignments;
  }

  class StrandsFor {
    constructor(strandsContext, initialCb, conditionCb, updateCb, bodyCb, initialVars) {
      this.strandsContext = strandsContext;
      this.initialCb = initialCb;
      this.conditionCb = conditionCb;
      this.updateCb = updateCb;
      this.bodyCb = bodyCb;
      this.initialVars = initialVars;
    }

    build() {
      const cfg = this.strandsContext.cfg;
      const mergeBlock = createBasicBlock(cfg, BlockType.MERGE);

      // Create a BRANCH block to handle phi node declarations
      const branchBlock = createBasicBlock(cfg, BlockType.BRANCH);
      addEdge(cfg, cfg.currentBlock, branchBlock);
      addEdge(cfg, branchBlock, mergeBlock);

      // Initialize loop variable phi node
      const { initialVar, phiNode } = this.initializeLoopVariable(cfg, branchBlock);

      // Execute condition and update callbacks to get nodes for analysis
      pushBlock(cfg, cfg.currentBlock);
      const loopVarNode = createStrandsNode(phiNode.id, phiNode.dimension, this.strandsContext);
      const conditionNode = this.conditionCb(loopVarNode);
      const updateResult = this.updateCb(loopVarNode);
      popBlock(cfg);

      // Check if loop has bounded iteration count
      const isBounded = this.loopIsBounded(initialVar, conditionNode, updateResult);

      if (isBounded) {
        this.buildBoundedLoop(cfg, branchBlock, mergeBlock, initialVar, phiNode, conditionNode, updateResult);
      } else {
        this.buildUnboundedLoop(cfg, branchBlock, mergeBlock, initialVar, phiNode, conditionNode, updateResult);
      }

      // Update the phi nodes created in buildBoundedLoop with actual body results
      const finalPhiNodes = this.phiNodesForBody;
      pushBlockForModification(cfg, branchBlock);
      for (const [varName, resultNode] of Object.entries(this.bodyResults)) {
        if (varName !== 'loopVar' && finalPhiNodes[varName]) {
          // Update the phi node's second input to use the actual body result
          const phiNodeID = finalPhiNodes[varName].id;
          const phiNodeData = getNodeDataFromID(this.strandsContext.dag, phiNodeID);
          // Update the dependsOn array to include the actual body result
          if (phiNodeData.dependsOn.length > 1) {
            phiNodeData.dependsOn[1] = resultNode.id;
          }
          if (phiNodeData.phiInputs && phiNodeData.phiInputs.length > 1) {
            phiNodeData.phiInputs[1].value = resultNode;
          }
        }
      }
      popBlock(cfg);

      // Create assignment nodes in the branch block for initial values
      pushBlockForModification(cfg, branchBlock);
      for (const [varName, initialValueNode] of Object.entries(this.initialVars)) {
        if (varName !== 'loopVar' && finalPhiNodes[varName]) {
          // Create an assignment statement: phiNode = initialValue
          const phiNodeID = finalPhiNodes[varName].id;
          const sourceNodeID = initialValueNode.id;
          // Create an assignment operation node for the initial value
          const assignmentNode = createNodeData({
            nodeType: NodeType.ASSIGNMENT,
            dependsOn: [phiNodeID, sourceNodeID],
            phiBlocks: []
          });
          const assignmentID = getOrCreateNode(this.strandsContext.dag, assignmentNode);
          recordInBasicBlock(cfg, branchBlock, assignmentID);
        }
      }
      popBlock(cfg);

      // Create assignment nodes in the final block after body execution (following conditionals pattern)
      // After executing the body callback, cfg.currentBlock should be the final block in the control flow
      pushBlockForModification(cfg, this.finalBodyBlock);
      for (const [varName, resultNode] of Object.entries(this.bodyResults)) {
        if (varName !== 'loopVar' && finalPhiNodes[varName]) {
          // Create an assignment statement: phiNode = bodyResult[varName]
          const phiNodeID = finalPhiNodes[varName].id;
          const sourceNodeID = resultNode.id;
          // Create an assignment operation node
          // Use dependsOn[0] for phiNodeID and dependsOn[1] for sourceNodeID
          // This represents: dependsOn[0] = dependsOn[1] (phiNode = sourceNode)
          const assignmentNode = createNodeData({
            nodeType: NodeType.ASSIGNMENT,
            dependsOn: [phiNodeID, sourceNodeID],
            phiBlocks: []
          });
          const assignmentID = getOrCreateNode(this.strandsContext.dag, assignmentNode);
          recordInBasicBlock(cfg, this.finalBodyBlock, assignmentID);
        }
      }
      popBlock(cfg);

      // Convert phi nodes to StrandsNodes for the final result
      const finalBodyResults = {};
      for (const [varName, phiNode] of Object.entries(finalPhiNodes)) {
        finalBodyResults[varName] = createStrandsNode(phiNode.id, phiNode.dimension, this.strandsContext);
      }

      pushBlock(cfg, mergeBlock);

      return finalBodyResults;
    }

    buildBoundedLoop(cfg, branchBlock, mergeBlock, initialVar, phiNode, conditionNode, updateResult) {
      // For bounded loops, create FOR block with three statements: init, condition, update
      const forBlock = createBasicBlock(cfg, BlockType.FOR);
      addEdge(cfg, branchBlock, forBlock);

      // Now add only the specific nodes we need to the FOR block
      pushBlock(cfg, forBlock);

      // 1. Init statement - assign initial value to phi node (or empty if no initializer)
      if (initialVar) {
        const initAssignmentNode = createNodeData({
          nodeType: NodeType.ASSIGNMENT,
          dependsOn: [phiNode.id, initialVar.id],
          phiBlocks: []
        });
        const initAssignmentID = getOrCreateNode(this.strandsContext.dag, initAssignmentNode);
        recordInBasicBlock(cfg, forBlock, initAssignmentID);
      }

      // 2. Condition statement - wrap in ExpressionStatement to force generation
      const conditionStatementNode = createNodeData({
        nodeType: NodeType.STATEMENT,
        statementType: StatementType.EXPRESSION,
        dependsOn: [conditionNode.id],
        phiBlocks: []
      });
      const conditionStatementID = getOrCreateNode(this.strandsContext.dag, conditionStatementNode);
      recordInBasicBlock(cfg, forBlock, conditionStatementID);

      // 3. Update statement - create assignment of update result to phi node
      const updateAssignmentNode = createNodeData({
        nodeType: NodeType.ASSIGNMENT,
        dependsOn: [phiNode.id, updateResult.id],
        phiBlocks: []
      });
      const updateAssignmentID = getOrCreateNode(this.strandsContext.dag, updateAssignmentNode);
      recordInBasicBlock(cfg, forBlock, updateAssignmentID);

      popBlock(cfg);

      // Verify we have the right number of statements (2 or 3 depending on initializer)
      const instructions = cfg.blockInstructions[forBlock] || [];
      const expectedLength = initialVar ? 3 : 2;
      if (instructions.length !== expectedLength) {
        throw new Error(`FOR block must have exactly ${expectedLength} statements, got ${instructions.length}`);
      }

      const scopeStartBlock = createBasicBlock(cfg, BlockType.SCOPE_START);
      addEdge(cfg, forBlock, scopeStartBlock);

      const bodyBlock = createBasicBlock(cfg, BlockType.DEFAULT);
      this.bodyBlock = bodyBlock;
      addEdge(cfg, scopeStartBlock, bodyBlock);

      this.executeBodyCallback(cfg, branchBlock, bodyBlock, phiNode);

      const scopeEndBlock = createBasicBlock(cfg, BlockType.SCOPE_END);
      addEdge(cfg, bodyBlock, scopeEndBlock);
      addEdge(cfg, scopeEndBlock, mergeBlock);
    }

    buildUnboundedLoop(cfg, branchBlock, mergeBlock, initialVar, phiNode, conditionNode, updateResult) {
      // For unbounded loops, create FOR block with infinite loop and break condition
      const forBlock = createBasicBlock(cfg, BlockType.FOR);
      addEdge(cfg, branchBlock, forBlock);

      // Create FOR block with three empty statements for for(;;) syntax
      pushBlock(cfg, forBlock);

      // 1. Init statement - initialize loop variable or empty
      if (initialVar) {
        const initAssignmentNode = createNodeData({
          nodeType: NodeType.ASSIGNMENT,
          dependsOn: [phiNode.id, initialVar.id],
          phiBlocks: []
        });
        const initAssignmentID = getOrCreateNode(this.strandsContext.dag, initAssignmentNode);
        recordInBasicBlock(cfg, forBlock, initAssignmentID);
      } else {
        // Create empty statement for init
        const emptyInitNode = createNodeData({
          nodeType: NodeType.STATEMENT,
          statementType: StatementType.EMPTY,
          dependsOn: [],
          phiBlocks: []
        });
        const emptyInitID = getOrCreateNode(this.strandsContext.dag, emptyInitNode);
        recordInBasicBlock(cfg, forBlock, emptyInitID);
      }

      // 2. Condition statement - empty for infinite loop
      const emptyConditionNode = createNodeData({
        nodeType: NodeType.STATEMENT,
        statementType: StatementType.EMPTY,
        dependsOn: [],
        phiBlocks: []
      });
      const emptyConditionID = getOrCreateNode(this.strandsContext.dag, emptyConditionNode);
      recordInBasicBlock(cfg, forBlock, emptyConditionID);

      // 3. Update statement - empty for infinite loop
      const emptyUpdateNode = createNodeData({
        nodeType: NodeType.STATEMENT,
        statementType: StatementType.EMPTY,
        dependsOn: [],
        phiBlocks: []
      });
      const emptyUpdateID = getOrCreateNode(this.strandsContext.dag, emptyUpdateNode);
      recordInBasicBlock(cfg, forBlock, emptyUpdateID);

      popBlock(cfg);

      const scopeStartBlock = createBasicBlock(cfg, BlockType.SCOPE_START);
      addEdge(cfg, forBlock, scopeStartBlock);

      // Add break condition check right after scope start
      const breakCheckBlock = createBasicBlock(cfg, BlockType.DEFAULT);
      addEdge(cfg, scopeStartBlock, breakCheckBlock);

      pushBlock(cfg, breakCheckBlock);

      // Generate break statement: if (!condition) break;
      // First, create the logical NOT of the condition: !condition
      const condition = conditionNode;
      const negatedCondition = this.createLogicalNotNode(condition);

      // Create a conditional break using the existing conditional structure
      // We'll create an IF_COND block that leads to a break statement
      const breakConditionBlock = createBasicBlock(cfg, BlockType.IF_COND);
      addEdge(cfg, breakCheckBlock, breakConditionBlock);
      cfg.blockConditions[breakConditionBlock] = negatedCondition.id;

      // Add scope start block for break statement
      const breakScopeStartBlock = createBasicBlock(cfg, BlockType.SCOPE_START);
      addEdge(cfg, breakConditionBlock, breakScopeStartBlock);

      const breakStatementBlock = createBasicBlock(cfg, BlockType.DEFAULT);
      addEdge(cfg, breakScopeStartBlock, breakStatementBlock);

      // Create the break statement in the break statement block
      pushBlock(cfg, breakStatementBlock);
      const breakStatementNode = createNodeData({
        nodeType: NodeType.STATEMENT,
        statementType: StatementType.BREAK,
        dependsOn: [],
        phiBlocks: []
      });
      const breakStatementID = getOrCreateNode(this.strandsContext.dag, breakStatementNode);
      recordInBasicBlock(cfg, breakStatementBlock, breakStatementID);
      popBlock(cfg);

      // Add scope end block for break statement
      const breakScopeEndBlock = createBasicBlock(cfg, BlockType.SCOPE_END);
      addEdge(cfg, breakStatementBlock, breakScopeEndBlock);

      // The break scope end block leads to the merge block (exits the loop)
      addEdge(cfg, breakScopeEndBlock, mergeBlock);

      popBlock(cfg);

      const bodyBlock = createBasicBlock(cfg, BlockType.DEFAULT);
      this.bodyBlock = bodyBlock;
      addEdge(cfg, breakCheckBlock, bodyBlock);

      this.executeBodyCallback(cfg, branchBlock, bodyBlock, phiNode);

      const updateBlock = createBasicBlock(cfg, BlockType.DEFAULT);
      addEdge(cfg, bodyBlock, updateBlock);

      // Update the loop variable in the update block (like bounded loops)
      pushBlock(cfg, updateBlock);
      const updateAssignmentNode = createNodeData({
        nodeType: NodeType.ASSIGNMENT,
        dependsOn: [phiNode.id, updateResult.id],
        phiBlocks: []
      });
      const updateAssignmentID = getOrCreateNode(this.strandsContext.dag, updateAssignmentNode);
      recordInBasicBlock(cfg, updateBlock, updateAssignmentID);
      popBlock(cfg);

      const scopeEndBlock = createBasicBlock(cfg, BlockType.SCOPE_END);
      addEdge(cfg, updateBlock, scopeEndBlock);

      // Connect end of for loop to the merge agter the loop
      addEdge(cfg, scopeEndBlock, mergeBlock);

      // Break condition exits to merge
      addEdge(cfg, breakCheckBlock, mergeBlock);
    }

    initializeLoopVariable(cfg, branchBlock) {
      pushBlock(cfg, branchBlock);
      let initialVar = this.initialCb();

      // Convert to StrandsNode if it's not already one
      if (!(initialVar instanceof StrandsNode)) {
        const { id, dimension } = primitiveConstructorNode(this.strandsContext, { baseType: BaseType.FLOAT, dimension: 1 }, initialVar);
        initialVar = createStrandsNode(id, dimension, this.strandsContext);
      }

      // Create phi node for the loop variable in the BRANCH block
      const phiNode = createPhiNode(this.strandsContext, [
        { value: initialVar, blockId: branchBlock },
        { value: initialVar, blockId: branchBlock } // Placeholder, will be updated later
      ], 'loopVar');
      popBlock(cfg);

      return { initialVar, phiNode };
    }

    createLogicalNotNode(conditionNode) {
      const notOperationNode = createNodeData({
        nodeType: NodeType.OPERATION,
        opCode: OpCode.Unary.LOGICAL_NOT,
        baseType: BaseType.BOOL,
        dimension: 1,
        dependsOn: [conditionNode.id],
        phiBlocks: [],
        usedBy: []
      });
      const notOperationID = getOrCreateNode(this.strandsContext.dag, notOperationNode);
      return createStrandsNode(notOperationID, 1, this.strandsContext);
    }

    executeBodyCallback(cfg, branchBlock, bodyBlock, phiNode) {
      pushBlock(cfg, bodyBlock);

      // Create phi node references to pass to the body callback
      const phiVars = {};
      const phiNodesForBody = {};
      pushBlockForModification(cfg, branchBlock);
      for (const [varName, initialValueNode] of Object.entries(this.initialVars)) {
        if (varName !== 'loopVar') {
          // Create phi node that will be used for the final result
          const varPhiNode = createPhiNode(this.strandsContext, [
            { value: initialValueNode, blockId: branchBlock }, // Initial value
            { value: initialValueNode, blockId: bodyBlock }     // Placeholder - will update after body execution
          ], varName);
          phiNodesForBody[varName] = varPhiNode;
          phiVars[varName] = createStrandsNode(varPhiNode.id, varPhiNode.dimension, this.strandsContext);
        }
      }
      popBlock(cfg);

      const loopVarNode = createStrandsNode(phiNode.id, phiNode.dimension, this.strandsContext);
      this.bodyResults = this.bodyCb(loopVarNode, phiVars) || {};
      for (const key in this.bodyResults) {
        this.bodyResults[key] = this.strandsContext.p5.strandsNode(this.bodyResults[key]);
      }
      this.phiNodesForBody = phiNodesForBody;
      // Capture the final block after body execution before popping
      this.finalBodyBlock = cfg.currentBlock;
      popBlock(cfg);
    }

    loopIsBounded(initialVar, conditionNode, updateVar) {
      // A loop is considered "bounded" if we can determine at compile time that it will
      // execute a known number of iterations. This happens when:
      // 1. The condition compares the loop variable against a compile-time constant
      // 2. At least one side of the comparison uses only literals (no variables/uniforms)

      if (!conditionNode) return false;

      // Analyze the condition node - it should be a comparison operation
      const conditionData = getNodeDataFromID(this.strandsContext.dag, conditionNode.id);

      if (conditionData.nodeType !== NodeType.OPERATION) {
        return false;
      }

      // For a comparison like "i < bound", we need at least one side to use only literals
      // The condition should have two dependencies: left and right operands
      if (!conditionData.dependsOn || conditionData.dependsOn.length !== 2) {
        return false;
      }

      // Check if either operand uses only literals
      const leftOperand = createStrandsNode(conditionData.dependsOn[0], 1, this.strandsContext);
      const rightOperand = createStrandsNode(conditionData.dependsOn[1], 1, this.strandsContext);

      const leftUsesOnlyLiterals = this.nodeUsesOnlyLiterals(leftOperand);
      const rightUsesOnlyLiterals = this.nodeUsesOnlyLiterals(rightOperand);

      // At least one side should use only literals for the loop to be bounded
      return leftUsesOnlyLiterals || rightUsesOnlyLiterals;
    }

    nodeUsesOnlyLiterals(node) {
      // Recursively check if a node and all its dependencies use only literals
      const nodeData = getNodeDataFromID(this.strandsContext.dag, node.id);

      switch (nodeData.nodeType) {
        case NodeType.LITERAL:
          return true;

        case NodeType.VARIABLE:
          // Variables (like uniforms) make this branch unbounded
          return false;

        case NodeType.PHI:
          // Phi nodes (like loop variables) are not literals
          return false;

        case NodeType.OPERATION:
          // For operations, all dependencies must use only literals
          if (nodeData.dependsOn) {
            for (const depId of nodeData.dependsOn) {
              const depNode = createStrandsNode(depId, 1, this.strandsContext);
              if (!this.nodeUsesOnlyLiterals(depNode)) {
                return false;
              }
            }
          }
          return true;

        default:
          // Conservative: if we don't know the node type, assume not literal
          return false;
      }
    }
  }

  const BUILTIN_GLOBAL_SPECS = {
    width: { typeInfo: DataType.float1, get: (p) => p.width },
    height: { typeInfo: DataType.float1, get: (p) => p.height },
    mouseX: { typeInfo: DataType.float1, get: (p) => p.mouseX },
    mouseY: { typeInfo: DataType.float1, get: (p) => p.mouseY },
    pmouseX: { typeInfo: DataType.float1, get: (p) => p.pmouseX },
    pmouseY: { typeInfo: DataType.float1, get: (p) => p.pmouseY },
    winMouseX: { typeInfo: DataType.float1, get: (p) => p.winMouseX },
    winMouseY: { typeInfo: DataType.float1, get: (p) => p.winMouseY },
    pwinMouseX: { typeInfo: DataType.float1, get: (p) => p.pwinMouseX },
    pwinMouseY: { typeInfo: DataType.float1, get: (p) => p.pwinMouseY },
    frameCount: { typeInfo: DataType.float1, get: (p) => p.frameCount },
    deltaTime: { typeInfo: DataType.float1, get: (p) => p.deltaTime },
    displayWidth: { typeInfo: DataType.float1, get: (p) => p.displayWidth },
    displayHeight: { typeInfo: DataType.float1, get: (p) => p.displayHeight },
    windowWidth: { typeInfo: DataType.float1, get: (p) => p.windowWidth },
    windowHeight: { typeInfo: DataType.float1, get: (p) => p.windowHeight },
    mouseIsPressed: { typeInfo: DataType.bool1, get: (p) => p.mouseIsPressed },
  };

  function _getBuiltinGlobalsCache(strandsContext) {
    if (!strandsContext._builtinGlobals || strandsContext._builtinGlobals.dag !== strandsContext.dag) {
      strandsContext._builtinGlobals = {
        dag: strandsContext.dag,
        nodes: new Map(),
        uniformsAdded: new Set(),
      };
    }
    // return the cache
    return strandsContext._builtinGlobals
  }

  function getBuiltinGlobalNode(strandsContext, name) {
    const spec = BUILTIN_GLOBAL_SPECS[name];
    if (!spec) return null
    
    const cache = _getBuiltinGlobalsCache(strandsContext);
    const uniformName = `_p5_global_${name}`;
    const cached = cache.nodes.get(uniformName);
    if (cached) return cached

    if (!cache.uniformsAdded.has(uniformName)) {
      cache.uniformsAdded.add(uniformName);
      strandsContext.uniforms.push({
        name: uniformName,
        typeInfo: spec.typeInfo,
        defaultValue: () => {
          const p5Instance = strandsContext.renderer?._pInst || strandsContext.p5?.instance;
          return p5Instance ? spec.get(p5Instance) : undefined
        },
      });
    }

    const { id, dimension } = variableNode(strandsContext, spec.typeInfo, uniformName);
    const node = createStrandsNode(id, dimension, strandsContext);
    node._originalBuiltinName = name;
    cache.nodes.set(uniformName, node);
    return node
  }

  function installBuiltinGlobalAccessors(strandsContext) {
    if (strandsContext._builtinGlobalsAccessorsInstalled) return

    const getRuntimeP5Instance = () => strandsContext.renderer?._pInst || strandsContext.p5?.instance;

    for (const name of Object.keys(BUILTIN_GLOBAL_SPECS)) {
      const spec = BUILTIN_GLOBAL_SPECS[name];
      Object.defineProperty(window, name, {
        get: () => {
          if (strandsContext.active) {
            return getBuiltinGlobalNode(strandsContext, name);
          }
          const inst = getRuntimeP5Instance();
            return spec.get(inst);
        },
      });
    }
    strandsContext._builtinGlobalsAccessorsInstalled = true;
  }

  //////////////////////////////////////////////
  // User nodes
  //////////////////////////////////////////////
  function initGlobalStrandsAPI(p5, fn, strandsContext) {
    // We augment the strands node with operations programatically
    // this means methods like .add, .sub, etc can be chained
    for (const { name, arity, opCode } of OperatorTable) {
      if (arity === 'binary') {
        StrandsNode.prototype[name] = function (...right) {
          const { id, dimension } = binaryOpNode(strandsContext, this, right, opCode);
          return createStrandsNode(id, dimension, strandsContext);
        };
      }
      if (arity === 'unary') {
        p5[name] = function (nodeOrValue) {
          const { id, dimension } = unaryOpNode(strandsContext, nodeOrValue, opCode);
          return createStrandsNode(id, dimension, strandsContext);
        };
      }
    }
    //////////////////////////////////////////////
    // Unique Functions
    //////////////////////////////////////////////
    fn.discard = function() {
      statementNode(strandsContext, StatementType.DISCARD);
    };
    fn.break = function() {
      statementNode(strandsContext, StatementType.BREAK);
    };
    p5.break = fn.break;
    fn.instanceID = function() {
      const node = variableNode(strandsContext, { baseType: BaseType.INT, dimension: 1 }, strandsContext.backend.instanceIdReference());
      return createStrandsNode(node.id, node.dimension, strandsContext);
    };
    // Internal methods use p5 static methods; user-facing methods use fn.
    // Some methods need to be used by both.
    p5.strandsIf = function(conditionNode, ifBody) {
      return new StrandsConditional(strandsContext, conditionNode, ifBody);
    };
    fn.strandsIf = p5.strandsIf;
    p5.strandsFor = function(initialCb, conditionCb, updateCb, bodyCb, initialVars) {
      return new StrandsFor(strandsContext, initialCb, conditionCb, updateCb, bodyCb, initialVars).build();
    };
    fn.strandsFor = p5.strandsFor;
    p5.strandsEarlyReturn = function(value) {
      const { dag, cfg } = strandsContext;

      // Ensure we're inside a hook
      if (!strandsContext.activeHook) {
        throw new Error('strandsEarlyReturn can only be used inside a hook callback');
      }

      // Convert value to a StrandsNode if it isn't already
      const valueNode = value instanceof StrandsNode ? value : p5.strandsNode(value);

      // Create a new CFG block for the early return
      const earlyReturnBlockID = createBasicBlock(cfg, BlockType.DEFAULT);
      addEdge(cfg, cfg.currentBlock, earlyReturnBlockID);
      pushBlock(cfg, earlyReturnBlockID);

      // Create the early return statement node
      const nodeData = createNodeData({
        nodeType: NodeType.STATEMENT,
        statementType: StatementType.EARLY_RETURN,
        dependsOn: [valueNode.id]
      });
      const earlyReturnID = getOrCreateNode(dag, nodeData);
      recordInBasicBlock(cfg, cfg.currentBlock, earlyReturnID);

      // Add the value to the hook's earlyReturns array for later type checking
      strandsContext.activeHook.earlyReturns.push({ earlyReturnID, valueNode });

      popBlock(cfg);

      return valueNode;
    };
    fn.strandsEarlyReturn = p5.strandsEarlyReturn;
    p5.strandsNode = function(...args) {
      if (args.length === 1 && args[0] instanceof StrandsNode) {
        return args[0];
      }
      if (args.length > 4) {
        userError("type error", "It looks like you've tried to construct a p5.strands node implicitly, with more than 4 components. This is currently not supported.");
      }
      const { id, dimension } = primitiveConstructorNode(strandsContext, { baseType: BaseType.FLOAT, dimension: null }, args.flat());
      return createStrandsNode(id, dimension, strandsContext);//new StrandsNode(id, dimension, strandsContext);
    };
    //////////////////////////////////////////////
    // Builtins, uniforms, variable constructors
    //////////////////////////////////////////////
    for (const [functionName, overrides] of Object.entries(strandsBuiltinFunctions)) {
      const isp5Function = overrides[0].isp5Function;
      if (isp5Function) {
        const originalFn = fn[functionName];
        fn[functionName] = function(...args) {
          if (strandsContext.active) {
            const { id, dimension } =  functionCallNode(strandsContext, functionName, args);
            return createStrandsNode(id, dimension, strandsContext);
          } else {
            return originalFn.apply(this, args);
          }
        };
      } else {
        fn[functionName] = function (...args) {
          if (strandsContext.active) {
            const { id, dimension } = functionCallNode(strandsContext, functionName, args);
            return createStrandsNode(id, dimension, strandsContext);
          } else {
            p5._friendlyError(
              `It looks like you've called ${functionName} outside of a shader's modify() function.`
            );
          }
        };
      }
    }

    fn.getTexture = function (...rawArgs) {
      if (strandsContext.active) {
        const { id, dimension } = strandsContext.backend.createGetTextureCall(strandsContext, rawArgs);
        return createStrandsNode(id, dimension, strandsContext);
      } else {
        p5._friendlyError(
          `It looks like you've called getTexture outside of a shader's modify() function.`
        );
      }
    };

    // Add texture function as alias for getTexture with p5 fallback
    const originalTexture = fn.texture;
    fn.texture = function (...args) {
      if (strandsContext.active) {
        return this.getTexture(...args);
      } else {
        return originalTexture.apply(this, args);
      }
    };

    // Add noise function with backend-agnostic implementation
    const originalNoise = fn.noise;
    const originalNoiseDetail = fn.noiseDetail;

    strandsContext._noiseOctaves = null;
    strandsContext._noiseAmpFalloff = null;

    fn.noiseDetail = function (lod, falloff = 0.5) {
      if (!strandsContext.active) {
        return originalNoiseDetail.apply(this, arguments);
      }

      strandsContext._noiseOctaves = lod;
      strandsContext._noiseAmpFalloff = falloff;
    };

    fn.noise = function (...args) {
      if (!strandsContext.active) {
        return originalNoise.apply(this, args); // fallback to regular p5.js noise
      }
      // Get noise shader snippet from the current renderer
      const noiseSnippet = this._renderer.getNoiseShaderSnippet();
      strandsContext.vertexDeclarations.add(noiseSnippet);
      strandsContext.fragmentDeclarations.add(noiseSnippet);

      // Make each input into a strands node so that we can check their dimensions
      const strandsArgs = args.flat().map(arg => p5.strandsNode(arg));
      let nodeArgs;
      if (strandsArgs.length === 3) {
        nodeArgs = [fn.vec3(strandsArgs[0], strandsArgs[1], strandsArgs[2])];
      } else if (strandsArgs.length === 2) {
        nodeArgs = [fn.vec3(strandsArgs[0], strandsArgs[1], 0)];
      } else if (strandsArgs.length === 1 && strandsArgs[0].dimension <= 3) {
        if (strandsArgs[0].dimension === 3) {
          nodeArgs = strandsArgs;
        } else if (strandsArgs[0].dimension === 2) {
          nodeArgs = [fn.vec3(strandsArgs[0], 0)];
        } else {
          nodeArgs = [fn.vec3(strandsArgs[0], 0, 0)];
        }
      } else {
        p5._friendlyError(
          `It looks like you've called noise() with ${args.length} arguments. It only supports 1D to 3D input.`
        );
      }

      const octaves = strandsContext._noiseOctaves !== null
        ? strandsContext._noiseOctaves
        : fn._getNoiseOctaves();
      const falloff = strandsContext._noiseAmpFalloff !== null
        ? strandsContext._noiseAmpFalloff
        : fn._getNoiseAmpFalloff();

      nodeArgs.push(octaves);
      nodeArgs.push(falloff);

      const { id, dimension } = functionCallNode(strandsContext, 'noise', nodeArgs, {
        overloads: [{
          params: [DataType.float3, DataType.int1, DataType.float1],
          returnType: DataType.float1,
        }]
      });
      return createStrandsNode(id, dimension, strandsContext);
    };

    // Next is type constructors and uniform functions.
    // For some of them, we have aliases so that you can write either a more human-readable
    // variant or also one more directly translated from GLSL, or to be more compatible with
    // APIs we documented at the release of 2.x and have to continue supporting.
    for (const type in DataType) {
      if (type === BaseType.DEFER || type === 'sampler') {
        continue;
      }
      const typeInfo = DataType[type];
      const typeAliases = [];
      let pascalTypeName;
      if (/^[ib]vec/.test(typeInfo.fnName)) {
        pascalTypeName = typeInfo.fnName
          .slice(0, 2).toUpperCase()
          + typeInfo.fnName
            .slice(2)
            .toLowerCase();
        typeAliases.push(pascalTypeName.replace('Vec', 'Vector'));
      } else {
        pascalTypeName = typeInfo.fnName.charAt(0).toUpperCase()
          + typeInfo.fnName.slice(1);
        if (pascalTypeName === 'Sampler2D') {
          typeAliases.push('Texture');
        } else if (/^vec/.test(typeInfo.fnName)) {
          typeAliases.push(pascalTypeName.replace('Vec', 'Vector'));
        }
      }
      fn[`uniform${pascalTypeName}`] = function(name, defaultValue) {
        const { id, dimension } = variableNode(strandsContext, typeInfo, name);
        strandsContext.uniforms.push({ name, typeInfo, defaultValue });
        return createStrandsNode(id, dimension, strandsContext);
      };
      // Shared variables with smart context detection
      fn[`shared${pascalTypeName}`] = function(name) {
        const { id, dimension } = variableNode(strandsContext, typeInfo, name);

        // Initialize shared variables tracking if not present
        if (!strandsContext.sharedVariables) {
          strandsContext.sharedVariables = new Map();
        }

        // Track this shared variable for smart declaration generation
        strandsContext.sharedVariables.set(name, {
          typeInfo,
          usedInVertex: false,
          usedInFragment: false,
          declared: false
        });

        return createStrandsNode(id, dimension, strandsContext);
      };

      // Alias varying* as shared* for backward compatibility
      fn[`varying${pascalTypeName}`] = fn[`shared${pascalTypeName}`];

      for (const typeAlias of typeAliases) {
        // For compatibility, also alias uniformVec2 as uniformVector2, what we initially
        // documented these as
        fn[`uniform${typeAlias}`] = fn[`uniform${pascalTypeName}`];
        fn[`varying${typeAlias}`] = fn[`varying${pascalTypeName}`];
        fn[`shared${typeAlias}`] = fn[`shared${pascalTypeName}`];
      }
      const originalp5Fn = fn[typeInfo.fnName];
      fn[typeInfo.fnName] = function(...args) {
        if (strandsContext.active) {
          if (args.length === 1 && args[0].dimension && args[0].dimension === typeInfo.dimension) {
            const { id, dimension } = functionCallNode(strandsContext, typeInfo.fnName, args, {
              overloads: [{
                params: [args[0].typeInfo()],
                returnType: typeInfo,
              }]
            });
            return createStrandsNode(id, dimension, strandsContext);
          } else {
            // For vector types with a single argument, repeat it for each component
            if (typeInfo.dimension > 1 && args.length === 1 && !Array.isArray(args[0]) &&
                !(args[0] instanceof StrandsNode && args[0].dimension > 1) &&
                (typeInfo.baseType === BaseType.FLOAT || typeInfo.baseType === BaseType.INT || typeInfo.baseType === BaseType.BOOL)) {
              args = Array(typeInfo.dimension).fill(args[0]);
            }
            const { id, dimension } = primitiveConstructorNode(strandsContext, typeInfo, args);
            return createStrandsNode(id, dimension, strandsContext);
          }
        } else if (originalp5Fn) {
          return originalp5Fn.apply(this, args);
        } else {
          p5._friendlyError(
            `It looks like you've called ${typeInfo.fnName} outside of a shader's modify() function.`
          );
        }
      };
    }
  }
  //////////////////////////////////////////////
  // Per-Hook functions
  //////////////////////////////////////////////
  function createHookArguments(strandsContext, parameters){
    const args = [];
    const dag = strandsContext.dag;
    for (const param of parameters) {
      if(isStructType(param.type)) {
        const structTypeInfo = structType(param);
        const { id, dimension } = structInstanceNode(strandsContext, structTypeInfo, param.name, []);
        const structNode = createStrandsNode(id, dimension, strandsContext).withStructProperties(
          structTypeInfo.properties.map(prop => prop.name)
        );
        for (let i = 0; i < structTypeInfo.properties.length; i++) {
          const propertyType = structTypeInfo.properties[i];
          Object.defineProperty(structNode, propertyType.name, {
            get() {
              const propNode = getNodeDataFromID(dag, dag.dependsOn[structNode.id][i]);
              const onRebind = (newFieldID) => {
                const oldDeps = dag.dependsOn[structNode.id];
                const newDeps = oldDeps.slice();
                newDeps[i] = newFieldID;
                const rebuilt = structInstanceNode(strandsContext, structTypeInfo, param.name, newDeps);
                structNode.id = rebuilt.id;
              };
              // TODO: implement member access operations
              // const { id, components } = createMemberAccessNode(strandsContext, structNode, componentNodes[i], componentTypeInfo.dataType);
              // const memberAccessNode = new StrandsNode(id, components);
              // return memberAccessNode;
              return createStrandsNode(propNode.id, propNode.dimension, strandsContext, onRebind);
            },
            set(val) {
              const oldDependsOn = dag.dependsOn[structNode.id];
              const newDependsOn = [...oldDependsOn];
              let newValueID;
              if (val instanceof StrandsNode) {
                newValueID = val.id;
              }
              else {
                let newVal = primitiveConstructorNode(strandsContext, propertyType.dataType, val);
                newValueID = newVal.id;
              }
              newDependsOn[i] = newValueID;
              const newStructInfo = structInstanceNode(strandsContext, structTypeInfo, param.name, newDependsOn);
              structNode.id = newStructInfo.id;
            }
          });
        }
        args.push(structNode);
      }
      else /*if(isNativeType(paramType.typeName))*/ {
        // Skip sampler parameters - they don't need strands nodes
        if (param.type.typeName === 'sampler') {
          continue;
        }
        if (!param.type.dataType) {
          throw new Error(`Missing dataType for parameter ${param.name} of type ${param.type.typeName}`);
        }
        const typeInfo = param.type.dataType;
        const { id, dimension } = variableNode(strandsContext, typeInfo, param.name);
        const arg = createStrandsNode(id, dimension, strandsContext);
        args.push(arg);
      }
    }
    return args;
  }
  function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName) {
    if (!(returned instanceof StrandsNode)) {
      // try {
        const result = primitiveConstructorNode(strandsContext, expectedType, returned);
        return result.id;
      // } catch (e) {
        // FES.userError('type error',
          // `There was a type mismatch for a value returned from ${hookName}.\n` +
          // `The value in question was supposed to be:\n` +
          // `${expectedType.baseType + expectedType.dimension}\n` +
          // `But you returned:\n` +
          // `${returned}`
        // );
      // }
    }
    const dag = strandsContext.dag;
    let returnedNodeID = returned.id;
    const receivedType = {
      baseType: dag.baseTypes[returnedNodeID],
      dimension: dag.dimensions[returnedNodeID],
    };
    if (receivedType.dimension !== expectedType.dimension) {
      if (receivedType.dimension !== 1) {
        userError('type error', `You have returned a vector with ${receivedType.dimension} components in ${hookName} when a ${expectedType.baseType + expectedType.dimension} was expected!`);
      }
      else {
        const result = primitiveConstructorNode(strandsContext, expectedType, returned);
        returnedNodeID = result.id;
      }
    }
    else if (receivedType.baseType !== expectedType.baseType) {
      const result = primitiveConstructorNode(strandsContext, expectedType, returned);
      returnedNodeID = result.id;
    }
    return returnedNodeID;
  }
  function createShaderHooksFunctions(strandsContext, fn, shader) {
    installBuiltinGlobalAccessors(strandsContext);

    // Add shader context to hooks before spreading
    const vertexHooksWithContext = Object.fromEntries(
      Object.entries(shader.hooks.vertex).map(([name, hook]) => [name, { ...hook, shaderContext: 'vertex' }])
    );
    const fragmentHooksWithContext = Object.fromEntries(
      Object.entries(shader.hooks.fragment).map(([name, hook]) => [name, { ...hook, shaderContext: 'fragment' }])
    );

    const availableHooks = {
      ...vertexHooksWithContext,
      ...fragmentHooksWithContext,
    };
    const hookTypes = Object.keys(availableHooks).map(name => shader.hookTypes(name));

    const { cfg, dag } = strandsContext;
    for (const hookType of hookTypes) {
      const hook = function(hookUserCallback) {
        const args = setupHook();
        hook._result = hookUserCallback(...args) ?? hook._result;
        finishHook();
      };

      // In the flat strands API, this is how result-returning hooks
      // are used
      hook.set = function(result) {
        hook._result = result;
      };

      let entryBlockID;
      function setupHook() {
        strandsContext.activeHook = hook;
        entryBlockID = createBasicBlock(cfg, BlockType.FUNCTION);
        addEdge(cfg, cfg.currentBlock, entryBlockID);
        pushBlock(cfg, entryBlockID);
        const args = createHookArguments(strandsContext, hookType.parameters);
        const numStructArgs = hookType.parameters.filter(param => param.type.properties).length;
        let argIdx = -1;
        if (numStructArgs === 1) {
          argIdx = hookType.parameters.findIndex(param => param.type.properties);
        }
        for (let i = 0; i < args.length; i++) {
          if (i === argIdx) {
            for (const key of args[argIdx].structProperties || []) {
              Object.defineProperty(hook, key, {
                get() {
                  return args[argIdx][key];
                },
                set(val) {
                  args[argIdx][key] = val;
                },
                enumerable: true,
              });
            }
            if (hookType.returnType?.typeName === hookType.parameters[argIdx].type.typeName) {
              hook.set(args[argIdx]);
            }
          } else {
            hook[hookType.parameters[i].name] = args[i];
          }
        }
        return args;
      }
      function finishHook() {
        const userReturned = hook._result;
        strandsContext.activeHook = undefined;

        const expectedReturnType = hookType.returnType;
        let rootNodeID = null;
        const handleRetVal = (retNode) => {
          if(isStructType(expectedReturnType)) {
            const expectedStructType = structType(expectedReturnType);
            if (retNode instanceof StrandsNode) {
              const returnedNode = getNodeDataFromID(strandsContext.dag, retNode.id);
              if (returnedNode.baseType !== expectedStructType.typeName) {
                userError("type error", `You have returned a ${retNode.baseType} from ${hookType.name} when a ${expectedStructType.typeName} was expected.`);
              }
              const newDeps = returnedNode.dependsOn.slice();
              for (let i = 0; i < expectedStructType.properties.length; i++) {
                const expectedType = expectedStructType.properties[i].dataType;
                const receivedNode = createStrandsNode(returnedNode.dependsOn[i], dag.dependsOn[retNode.id], strandsContext);
                newDeps[i] = enforceReturnTypeMatch(strandsContext, expectedType, receivedNode, hookType.name);
              }
              dag.dependsOn[retNode.id] = newDeps;
              return retNode.id;
            }
            else {
              const expectedProperties = expectedStructType.properties;
              const newStructDependencies = [];
              for (let i = 0; i < expectedProperties.length; i++) {
                const expectedProp = expectedProperties[i];
                const propName = expectedProp.name;
                const receivedValue = retNode[propName];
                if (receivedValue === undefined) {
                  userError('type error', `You've returned an incomplete struct from ${hookType.name}.\n` +
                    `Expected: { ${expectedReturnType.properties.map(p => p.name).join(', ')} }\n` +
                    `Received: { ${Object.keys(retNode).join(', ')} }\n` +
                    `All of the properties are required!`);
                }
                const expectedTypeInfo = expectedProp.dataType;
                const returnedPropID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, receivedValue, hookType.name);
                newStructDependencies.push(returnedPropID);
              }
              const newStruct = structConstructorNode(strandsContext, expectedStructType, newStructDependencies);
              return newStruct.id;
            }
          }
          else /*if(isNativeType(expectedReturnType.typeName))*/ {
            if (!expectedReturnType.dataType) {
              throw new Error(`Missing dataType for return type ${expectedReturnType.typeName}`);
            }
            const expectedTypeInfo = expectedReturnType.dataType;
            return enforceReturnTypeMatch(strandsContext, expectedTypeInfo, retNode, hookType.name);
          }
        };
        for (const { valueNode, earlyReturnID } of hook.earlyReturns) {
          const id = handleRetVal(valueNode);
          dag.dependsOn[earlyReturnID] = [id];
        }
        rootNodeID = userReturned ? handleRetVal(userReturned) : undefined;
        const fullHookName = `${hookType.returnType.typeName} ${hookType.name}`;
        const hookInfo = availableHooks[fullHookName];
        strandsContext.hooks.push({
          hookType,
          entryBlockID,
          rootNodeID,
          shaderContext: hookInfo?.shaderContext, // 'vertex' or 'fragment'
        });
        popBlock(cfg);
      }    hook.begin = setupHook;
      hook.end = finishHook;

      const aliases = [hookType.name];
      if (strandsContext.baseShader?.hooks?.hookAliases?.[hookType.name]) {
        aliases.push(...strandsContext.baseShader.hooks.hookAliases[hookType.name]);
      }

      // If the hook has a name like getPixelInputs, create an alias without
      // the get* prefix, like pixelInputs
      const nameMatch = /^get([A-Z0-9]\w*)$/.exec(hookType.name);
      if (nameMatch) {
        const unprefixedName = nameMatch[1][0].toLowerCase() + nameMatch[1].slice(1);
        if (!fn[unprefixedName]) {
          aliases.push(unprefixedName);
        }
      }

      for (const name of aliases) {
        strandsContext.windowOverrides[name] = window[name];
        strandsContext.fnOverrides[name] = fn[name];
        window[name] = hook;
        fn[name] = hook;
      }
      hook.earlyReturns = [];
    }
  }

  /**
   * @module 3D
   * @submodule p5.strands
   * @for p5
   * @requires core
   */


  function strands(p5, fn) {
    // Whether or not strands callbacks should be forced to be executed in global mode.
    // This is turned on while loading shaders from files, when there is not a feasible
    // way to pass context in.
    fn._runStrandsInGlobalMode = false;

    //////////////////////////////////////////////
    // Global Runtime
    //////////////////////////////////////////////
    function initStrandsContext(
      ctx,
      backend,
      { active = false, renderer = null, baseShader = null } = {},
    ) {
      ctx.dag = createDirectedAcyclicGraph();
      ctx.cfg = createControlFlowGraph();
      ctx.uniforms = [];
      ctx.vertexDeclarations = new Set();
      ctx.fragmentDeclarations = new Set();
      ctx.hooks = [];
      ctx.globalAssignments = [];
      ctx.backend = backend;
      ctx.active = active;
      ctx.renderer = renderer;
      ctx.baseShader = baseShader;
      ctx.previousFES = p5.disableFriendlyErrors;
      ctx.windowOverrides = {};
      ctx.fnOverrides = {};
      if (active) {
        p5.disableFriendlyErrors = true;
      }
      ctx.p5 = p5;
    }

    function deinitStrandsContext(ctx) {
      ctx.dag = createDirectedAcyclicGraph();
      ctx.cfg = createControlFlowGraph();
      ctx.uniforms = [];
      ctx.vertexDeclarations = new Set();
      ctx.fragmentDeclarations = new Set();
      ctx.hooks = [];
      ctx.globalAssignments = [];
      ctx.active = false;
      p5.disableFriendlyErrors = ctx.previousFES;
      for (const key in ctx.windowOverrides) {
        window[key] = ctx.windowOverrides[key];
      }
      for (const key in ctx.fnOverrides) {
        fn[key] = ctx.fnOverrides[key];
      }
    }

    const strandsContext = {};
    initStrandsContext(strandsContext);
    initGlobalStrandsAPI(p5, fn, strandsContext);

    function withTempGlobalMode(pInst, callback) {
      if (pInst._isGlobal) return callback();

      const prev = {};
      for (const key of Object.getOwnPropertyNames(fn)) {
        const descriptor = Object.getOwnPropertyDescriptor(fn, key);
        if (descriptor && !descriptor.get && typeof fn[key] === "function") {
          prev[key] = window[key];
          window[key] = fn[key].bind(pInst);
        }
      }

      try {
        callback();
      } finally {
        for (const key in prev) {
          window[key] = prev[key];
        }
      }
    }

    //////////////////////////////////////////////
    // Entry Point
    //////////////////////////////////////////////
    const oldModify = p5.Shader.prototype.modify;

    p5.Shader.prototype.modify = function (shaderModifier, scope = {}) {
      try {
        if (
          shaderModifier instanceof Function ||
          typeof shaderModifier === "string"
        ) {
          // Reset the context object every time modify is called;
          // const backend = glslBackend;
          initStrandsContext(strandsContext, this._renderer.strandsBackend, {
            active: true,
            renderer: this._renderer,
            baseShader: this,
          });
          createShaderHooksFunctions(strandsContext, fn, this);
          // TODO: expose this, is internal for debugging for now.
          const options = { srcLocations: false };

          // 1. Transpile from strands DSL to JS
          let strandsCallback;
          {
            // #7955 Wrap function declaration code in brackets so anonymous functions are not top level statements, which causes an error in acorn when parsing
            // https://github.com/acornjs/acorn/issues/1385
            const sourceString =
              typeof shaderModifier === "string"
                ? `(${shaderModifier})`
                : `(${shaderModifier.toString()})`;
            strandsCallback = transpileStrandsToJS(
              p5,
              sourceString,
              options.srcLocations,
              scope,
            );
          }

          // 2. Build the IR from JavaScript API
          const globalScope = createBasicBlock(
            strandsContext.cfg,
            BlockType.GLOBAL,
          );
          pushBlock(strandsContext.cfg, globalScope);
          if (strandsContext.renderer?._pInst?._runStrandsInGlobalMode) {
            withTempGlobalMode(strandsContext.renderer._pInst, strandsCallback);
          } else {
            strandsCallback();
          }
          popBlock(strandsContext.cfg);

          // 3. Generate shader code hooks object from the IR
          // .......
          const hooksObject = generateShaderCode(strandsContext);

          // Call modify with the generated hooks object
          return oldModify.call(this, hooksObject);
        } else {
          return oldModify.call(this, shaderModifier);
        }
      } finally {
        // Reset the strands runtime context
        deinitStrandsContext(strandsContext);
      }
    };
  }

  if (typeof p5 !== "undefined") {
    p5.registerAddon(strands);
  }

  /* ------------------------------------------------------------- */
  /**
   * @property {Object} worldInputs
   * @description
   * A shader hook block that modifies the world-space properties of each vertex in a shader. This hook can be used inside <a href="#/p5/buildColorShader">`buildColorShader()`</a> and similar shader <a href="#/p5.Shader/modify">`modify()`</a> calls to customize vertex positions, normals, texture coordinates, and colors before rendering. Modifications happen between the `.begin()` and `.end()` methods of the hook. "World space" refers to the coordinate system of the 3D scene, before any camera or projection transformations are applied.
   *
   * `worldInputs` has the following properties:
   * - `position`: a three-component vector representing the original position of the vertex.
   * - `normal`: a three-component vector representing the direction the surface is facing.
   * - `texCoord`: a two-component vector representing the texture coordinates.
   * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha).
   *
   * This hook is available in:
   * - <a href="#/p5/buildMaterialShader">`buildMaterialShader()`</a>
   * - <a href="#/p5/buildNormalShader">`buildNormalShader()`</a>
   * - <a href="#/p5/buildColorShader">`buildColorShader()`</a>
   * - <a href="#/p5/buildStrokeShader">`buildStrokeShader()`</a>
   *
   * @example
   * let myShader;
   * function setup() {
   *   createCanvas(200, 200, WEBGL);
   *   myShader = buildMaterialShader(material);
   * }
   *
   * function material() {
   *   let t = uniformFloat();
   *   worldInputs.begin();
   *   // Move the vertex up and down in a wave in world space
   *   // In world space, moving the object (e.g., with translate()) will affect these coordinates
   *   // The sphere is ~50 units tall here, so 20 gives a noticeable wave
   *   worldInputs.position.y += 20 * sin(t * 0.001 + worldInputs.position.x * 0.05);
   *   worldInputs.end();
   * }
   *
   * function draw() {
   *   background(255);
   *   shader(myShader);
   *   myShader.setUniform('t', millis());
   *   lights();
   *   noStroke();
   *   fill('red');
   *   sphere(50);
   * }
   */

  /**
   * @property {Object} combineColors
   * @description
   * A shader hook block that modifies how color components are combined in the fragment shader. This hook can be used inside <a href="#/p5/buildMaterialShader">`buildMaterialShader()`</a> and similar shader <a href="#/p5.Shader/modify">`modify()`</a> calls to control the final color output of a material. Modifications happen between the `.begin()` and `.end()` methods of the hook.
   *
   * `combineColors` has the following properties:
   *
   * - `baseColor`: a three-component vector representing the base color (red, green, blue).
   * - `diffuse`: a single number representing the diffuse reflection.
   * - `ambientColor`: a three-component vector representing the ambient color.
   * - `ambient`: a single number representing the ambient reflection.
   * - `specularColor`: a three-component vector representing the specular color.
   * - `specular`: a single number representing the specular reflection.
   * - `emissive`: a three-component vector representing the emissive color.
   * - `opacity`: a single number representing the opacity.
   *
   * Call `.set()` on the hook with a vector with four components (red, green, blue, alpha) for the final color.
   *
   * This hook is available in:
   * - <a href="#/p5/buildMaterialShader">`buildMaterialShader()`</a>
   *
   * @example
   * let myShader;
   * function setup() {
   *   createCanvas(200, 200, WEBGL);
   *   myShader = buildMaterialShader(material);
   * }
   *
   * function material() {
   *   combineColors.begin();
   *   // Custom color combination: add a green tint using vector properties
   *   combineColors.set([
   *     combineColors.baseColor * combineColors.diffuse +
   *       combineColors.ambientColor * combineColors.ambient +
   *       combineColors.specularColor * combineColors.specular +
   *       combineColors.emissive +
   *       [0, 0.2, 0], // Green tint
   *     combineColors.opacity
   *   ]);
   *   combineColors.end();
   * }
   *
   * function draw() {
   *   background(255);
   *   shader(myShader);
   *   lights();
   *   noStroke();
   *   fill('white');
   *   sphere(50);
   * }
   */

  /**
   * @method smoothstep
   * @description
   * A shader function that performs smooth Hermite interpolation between `0.0`
   * and `1.0`.
   *
   * This function is equivalent to the GLSL built-in
   * `smoothstep(edge0, edge1, x)` and is available inside p5.strands shader
   * callbacks. It is commonly used to create soft transitions, smooth edges,
   * fades, and anti-aliased effects.
   *
   * Smoothstep is useful when a threshold or cutoff is needed, but with a
   * gradual transition instead of a hard edge.
   *
   * - Returns `0.0` when `x` is less than or equal to `edge0`
   * - Returns `1.0` when `x` is greater than or equal to `edge1`
   * - Smoothly interpolates between `0.0` and `1.0` when `x` is between them
   *
   * @param {Number} edge0
   *        Lower edge of the transition
   * @param {Number} edge1
   *        Upper edge of the transition
   * @param {Number} x
   *        Input value to interpolate
   *
   * @returns {Number}
   *          A value between `0.0` and `1.0`
   *
   * @example
   * <div modernizr="webgl">
   * <code>
   * // Example 1: A soft vertical fade using smoothstep (no uniforms)
   *
   * let fadeShader;
   *
   * function fadeCallback() {
   *   getColor((inputs) => {
   *     // x goes from 0 → 1 across the canvas
   *     let x = inputs.texCoord.x;
   *
   *     // smoothstep creates a soft transition instead of a hard edge
   *     let t = smoothstep(0.25, 0.35, x);
   *
   *     // Use t directly as brightness
   *     return [t, t, t, 1];
   *   });
   * }
   *
   * function setup() {
   *   createCanvas(300, 200, WEBGL);
   *   fadeShader = baseFilterShader().modify(fadeCallback);
   * }
   *
   * function draw() {
   *   background(0);
   *   filter(fadeShader);
   * }
   * </code>
   * </div>
   *
   * @example
   * <div modernizr="webgl">
   * <code>
   * // Example 2: Animate the smooth transition using a uniform
   *
   * let animatedShader;
   *
   * function animatedFadeCallback() {
   *   const time = uniformFloat(() => millis() * 0.001);
   *
   *   getColor((inputs) => {
   *     let x = inputs.texCoord.x;
   *
   *     // Move the smoothstep band back and forth over time
   *     let center = 0.5 + 0.25 * sin(time);
   *     let t = smoothstep(center - 0.05, center + 0.05, x);
   *
   *     return [t, t, t, 1];
   *   });
   * }
   *
   * function setup() {
   *   createCanvas(300, 200, WEBGL);
   *   animatedShader = baseFilterShader().modify(animatedFadeCallback);
   * }
   *
   * function draw() {
   *   background(0);
   *   filter(animatedShader);
   * }
   * </code>
   * </div>
   */

  /**
   * @method beforeVertex
   * @private
   * @description
   * Registers a callback to run custom code at the very start of the vertex shader. This hook can be used inside <a href="#/p5/baseColorShader">baseColorShader()</a>.modify() and similar shader <a href="#/p5.Shader/modify">modify()</a> calls to set up variables or perform calculations that affect every vertex before processing begins. The callback receives no arguments.
   *
   * Note: This hook is currently limited to per-vertex operations; storing variables for later use is not supported.
   *
   * This hook is available in:
   * - <a href="#/p5/baseColorShader">baseColorShader()</a>
   * - <a href="#/p5/baseMaterialShader">baseMaterialShader()</a>
   * - <a href="#/p5/baseNormalShader">baseNormalShader()</a>
   * - <a href="#/p5/baseStrokeShader">baseStrokeShader()</a>
   *
   * @param {Function} callback
   *        A callback function which is called before each vertex is processed.
   */

  /**
   * @method afterVertex
   * @private
   * @description
   * Registers a callback to run custom code at the very end of the vertex shader. This hook can be used inside <a href="#/p5/baseColorShader">baseColorShader()</a>.modify() and similar shader <a href="#/p5.Shader/modify">modify()</a> calls to perform cleanup or final calculations after all vertex processing is done. The callback receives no arguments.
   *
   * Note: This hook is currently limited to per-vertex operations; storing variables for later use is not supported.
   *
   * This hook is available in:
   * - <a href="#/p5/baseColorShader">baseColorShader()</a>
   * - <a href="#/p5/baseMaterialShader">baseMaterialShader()</a>
   * - <a href="#/p5/baseNormalShader">baseNormalShader()</a>
   * - <a href="#/p5/baseStrokeShader">baseStrokeShader()</a>
   *
   * @param {Function} callback
   *        A callback function which is called after each vertex is processed.
   */

  /**
   * @method beforeFragment
   * @private
   * @description
   * Registers a callback to run custom code at the very start of the fragment shader. This hook can be used inside <a href="#/p5/baseColorShader">baseColorShader()</a>.modify() and similar shader <a href="#/p5.Shader/modify">modify()</a> calls to set up variables or perform calculations that affect every pixel before color calculations begin. The callback receives no arguments.
   *
   * This hook is available in:
   * - <a href="#/p5/baseColorShader">baseColorShader()</a>
   * - <a href="#/p5/baseMaterialShader">baseMaterialShader()</a>
   * - <a href="#/p5/baseNormalShader">baseNormalShader()</a>
   * - <a href="#/p5/baseStrokeShader">baseStrokeShader()</a>
   *
   * @param {Function} callback
   *        A callback function which is called before each fragment is processed.
   *
   * @example
   * let myShader;
   * function setup() {
   *   createCanvas(200, 200, WEBGL);
   *   myShader = baseColorShader().modify(() => {
   *     beforeFragment(() => {
   *       // Set a value for use in getFinalColor
   *       this.brightness = 0.5 + 0.5 * sin(millis() * 0.001);
   *     });
   *     getFinalColor(color => {
   *       // Use the value set in beforeFragment to tint the color
   *       color.r *= this.brightness; // Tint red channel
   *       return color;
   *     });
   *   });
   * }
   *
   * function draw() {
   *   background(220);
   *   shader(myShader);
   *   noStroke();
   *   fill('teal');
   *   box(100);
   * }
   */

  /**
   * @property {Object} pixelInputs
   * @description
   * A shader hook block that modifies the properties of each pixel before the final color is calculated. This hook can be used inside <a href="#/p5/buildMaterialShader">`buildMaterialShader()`</a> and similar shader <a href="#/p5.Shader/modify">`modify()`</a> calls to adjust per-pixel data before lighting is applied. Modifications happen between the `.begin()` and `.end()` methods of the hook.
   *
   * The properties of `pixelInputs` depend on the shader:
   *
   * - In <a href="#/p5/buildMaterialShader">`buildMaterialShader()`</a>:
   *   - `normal`: a three-component vector representing the surface normal.
   *   - `texCoord`: a two-component vector representing the texture coordinates (u, v).
   *   - `ambientLight`: a three-component vector representing the ambient light color.
   *   - `ambientMaterial`: a three-component vector representing the material's ambient color.
   *   - `specularMaterial`: a three-component vector representing the material's specular color.
   *   - `emissiveMaterial`: a three-component vector representing the material's emissive color.
   *   - `color`: a four-component vector representing the base color (red, green, blue, alpha).
   *   - `shininess`: a number controlling specular highlights.
   *   - `metalness`: a number controlling the metalness factor.
   *
   * - In <a href="#/p5/buildStrokeShader">`buildStrokeShader()`</a>:
   *   - `color`: a four-component vector representing the stroke color (red, green, blue, alpha).
   *   - `tangent`: a two-component vector representing the stroke tangent.
   *   - `center`: a two-component vector representing the cap/join center.
   *   - `position`: a two-component vector representing the current fragment position.
   *   - `strokeWeight`: a number representing the stroke weight in pixels.
   *
   * This hook is available in:
   * - <a href="#/p5/buildMaterialShader">`buildMaterialShader()`</a>
   * - <a href="#/p5/buildStrokeShader">`buildStrokeShader()`</a>
   *
   * @example
   * let myShader;
   * function setup() {
   *   createCanvas(200, 200, WEBGL);
   *   myShader = buildMaterialShader(material);
   * }
   *
   * function material() {
   *   let t = uniformFloat();
   *   pixelInputs.begin();
   *   // Animate alpha (transparency) based on x position
   *   pixelInputs.color.a = 0.5 + 0.5 *
   *     sin(pixelInputs.texCoord.x * 10.0 + t * 0.002);
   *   pixelInputs.end();
   * }
   *
   * function draw() {
   *   background(240);
   *   shader(myShader);
   *   myShader.setUniform('t', millis());
   *   lights();
   *   noStroke();
   *   fill('purple');
   *   circle(0, 0, 100);
   * }
   */

  /**
   * @method shouldDiscard
   * @private
   * @description
   * Registers a callback to decide whether to discard (skip drawing) a fragment (pixel) in the fragment shader. This hook can be used inside <a href="#/p5/baseStrokeShader">baseStrokeShader()</a>.modify() and similar shader <a href="#/p5.Shader/modify">modify()</a> calls to create effects like round points or custom masking. The callback receives a boolean:
   * - `willDiscard`: true if the fragment would be discarded by default
   *
   * Return true to discard the fragment, or false to keep it.
   *
   * This hook is available in:
   * - <a href="#/p5/baseStrokeShader">baseStrokeShader()</a>
   *
   * @param {Function} callback
   *        A callback function which receives a boolean and should return a boolean.
   *
   * @example
   * let myShader;
   * function setup() {
   *   createCanvas(200, 200, WEBGL);
   *   myShader = baseStrokeShader().modify({
   *      'bool shouldDiscard': '(bool outside) { return outside; }'
   *   });
   * }
   *
   * function draw() {
   *   background(255);
   *   strokeShader(myShader);
   *   strokeWeight(30);
   *   line(-width/3, 0, width/3, 0);
   * }
   */

  /**
   * @property finalColor
   * @description
   * A shader hook block that modifies the final color of each pixel after all lighting is applied. This hook can be used inside <a href="#/p5/buildMaterialShader">`buildMaterialShader()`</a> and similar shader <a href="#/p5.Shader/modify">`modify()`</a> calls to adjust the color before it appears on the screen. Modifications happen between the `.begin()` and `.end()` methods of the hook.
   *
   * `finalColor` has the following properties:
   * - `color`: a four-component vector representing the pixel color (red, green, blue, alpha).
   *
   * Call `.set()` on the hook with a vector with four components (red, green, blue, alpha) to update the final color.
   *
   * This hook is available in:
   * - <a href="#/p5/buildColorShader">`buildColorShader()`</a>
   * - <a href="#/p5/buildMaterialShader">`buildMaterialShader()`</a>
   * - <a href="#/p5/buildNormalShader">`buildNormalShader()`</a>
   * - <a href="#/p5/buildStrokeShader">`buildStrokeShader()`</a>
   *
   * @example
   * let myShader;
   * function setup() {
   *   createCanvas(200, 200, WEBGL);
   *   myShader = buildMaterialShader(material);
   * }
   *
   * function material() {
   *   finalColor.begin();
   *   let color = finalColor.color;
   *   // Add a blue tint to the output color
   *   color.b += 0.4;
   *   finalColor.set(color);
   *   finalColor.end();
   * }
   *
   * function draw() {
   *   background(230);
   *   shader(myShader);
   *   noStroke();
   *   fill('green');
   *   circle(0, 0, 100);
   * }
   */

  /**
   * @method afterFragment
   * @private
   * @description
   * Registers a callback to run custom code at the very end of the fragment shader. This hook can be used inside <a href="#/p5/baseColorShader">baseColorShader()</a>.modify() and similar shader <a href="#/p5.Shader/modify">modify()</a> calls to perform cleanup or final per-pixel effects after all color calculations are done. The callback receives no arguments.
   *
   * This hook is available in:
   * - <a href="#/p5/baseColorShader">baseColorShader()</a>
   * - <a href="#/p5/baseMaterialShader">baseMaterialShader()</a>
   * - <a href="#/p5/baseNormalShader">baseNormalShader()</a>
   * - <a href="#/p5/baseStrokeShader">baseStrokeShader()</a>
   *
   * @param {Function} callback
   *        A callback function which is called after each fragment is processed.
   *
   * @example
   * let myShader;
   * function setup() {
   *   createCanvas(200, 200, WEBGL);
   *   myShader = baseColorShader().modify(() => {
   *     getFinalColor(color => {
   *       // Add a purple tint to the color
   *       color.b += 0.2;
   *       return color;
   *     });
   *     afterFragment(() => {
   *       // This hook runs after the final color is set for each fragment.
   *       // You could use this for debugging or advanced effects.
   *     });
   *   });
   * }
   *
   * function draw() {
   *   background(240);
   *   shader(myShader);
   *   noStroke();
   *   fill('purple');
   *   sphere(60);
   * }
   */

  /**
   * @property {Object} filterColor
   * @description
   * A shader hook block that sets the color for each pixel in a filter shader. This hook can be used inside <a href="#/p5/buildFilterShader">`buildFilterShader()`</a> to control the output color for each pixel.
   *
   * `filterColor` has the following properties:
   * - `texCoord`: a two-component vector representing the texture coordinates (u, v).
   * - `canvasSize`: a two-component vector representing the canvas size in pixels (width, height).
   * - `texelSize`: a two-component vector representing the size of a single texel in texture space.
   * - `canvasContent`: a texture containing the sketch's contents before the filter is applied.
   *
   * Call `.set()` on the hook with a vector with four components (red, green, blue, alpha) to update the final color.
   *
   * This hook is available in:
   * - <a href="#/p5/buildFilterShader">`buildFilterShader()`</a>
   *
   * @example
   * let myShader;
   * function setup() {
   *   createCanvas(200, 200, WEBGL);
   *   myShader = buildFilterShader(warp);
   * }
   *
   * function warp() {
   *   filterColor.begin();
   *   // Warp the texture coordinates for a wavy effect
   *   let warped = [
   *     filterColor.texCoord.x,
   *     filterColor.texCoord.y + 0.1 * sin(filterColor.texCoord.x * 10)
   *   ];
   *   filterColor.set(getTexture(canvasContent, warped));
   *   filterColor.end();
   * }
   *
   * function draw() {
   *   background(180);
   *   // Draw something to the canvas
   *   fill('yellow');
   *   circle(0, 0, 150);
   *   filter(myShader);
   * }
   */

  /**
   * @property {Object} objectInputs
   * @description
   * A shader hook block to modify the properties of each vertex before any transformations are applied. This hook can be used inside <a href="#/p5/buildMaterialShader">`buildMaterialShader()`</a> and similar shader <a href="#/p5.Shader/modify">`modify()`</a> calls to customize vertex positions, normals, texture coordinates, and colors before rendering. Modifications happen between the `.begin()` and `.end()` methods of the hook. "Object space" refers to the coordinate system of the 3D scene before any transformations, cameras, or projection transformations are applied.
   *
   * `objectInputs` has the following properties:
   * - `position`: a three-component vector representing the original position of the vertex.
   * - `normal`: a three-component vector representing the direction the surface is facing.
   * - `texCoord`: a two-component vector representing the texture coordinates.
   * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha).
   *
   * This hook is available in:
   * - <a href="#/p5/buildColorShader">`buildColorShader()`</a>
   * - <a href="#/p5/buildMaterialShader">`buildMaterialShader()`</a>
   * - <a href="#/p5/buildNormalShader">`buildNormalShader()`</a>
   * - <a href="#/p5/buildStrokeShader">`buildStrokeShader()`</a>
   *
   * @example
   * let myShader;
   * function setup() {
   *   createCanvas(200, 200, WEBGL);
   *   myShader = buildMaterialShader(material);
   * }
   *
   * function material() {
   *   let t = uniformFloat();
   *   objectInputs.begin();
   *   // Create a sine wave along the object
   *   objectInputs.position.y += sin(t * 0.001 + objectInputs.position.x);
   *   objectInputs.end();
   * }
   *
   * function draw() {
   *   background(220);
   *   shader(myShader);
   *   myShader.setUniform('t', millis());
   *   noStroke();
   *   fill('orange');
   *   sphere(50);
   * }
   */

  /**
   * @property {Object} cameraInputs
   * @description
   * A shader hook block that adjusts vertex properties from the perspective of the camera. This hook can be used inside <a href="#/p5/buildMaterialShader">`buildMaterialShader()`</a> and similar shader <a href="#/p5.Shader/modify">`modify()`</a> calls to customize vertex positions, normals, texture coordinates, and colors before rendering. "Camera space" refers to the coordinate system of the 3D scene after transformations have been applied, seen relative to the camera.
   *
   * `cameraInputs`  has the following properties:
   * - `position`: a three-component vector representing the position after camera transformation.
   * - `normal`: a three-component vector representing the normal after camera transformation.
   * - `texCoord`: a two-component vector representing the texture coordinates.
   * - `color`: a four-component vector representing the color of the vertex (red, green, blue, alpha).
   *
   * This hook is available in:
   * - <a href="#/p5/buildColorShader">`buildColorShader()`</a>
   * - <a href="#/p5/buildMaterialShader">`buildMaterialShader()`</a>
   * - <a href="#/p5/buildNormalShader">`buildNormalShader()`</a>
   * - <a href="#/p5/buildStrokeShader">`buildStrokeShader()`</a>
   *
   * @example
   * let myShader;
   * function setup() {
   *   createCanvas(200, 200, WEBGL);
   *   myShader = buildMaterialShader(material);
   * }
   *
   * function material() {
   *   let t = uniformFloat();
   *   cameraInputs.begin();
   *   // Move vertices in camera space based on their x position
   *   cameraInputs.position.y += 30 * sin(cameraInputs.position.x * 0.05 + t * 0.001);
   *   // Tint all vertices blue
   *   cameraInputs.color.b = 1;
   *   cameraInputs.end();
   * }
   *
   * function draw() {
   *   background(200);
   *   shader(myShader);
   *   myShader.setUniform('t', millis());
   *   noStroke();
   *   fill('red');
   *   sphere(50);
   * }
   */

  /**
   * Retrieves the current color of a given texture at given coordinates.
   *
   * The given coordinates should be between [0, 0] representing the top-left of
   * the texture, and [1, 1] representing the bottom-right of the texture.
   *
   * The given texture could be, for example:
   * * <a href="#/p5.Image">p5.Image</a>,
   * * a <a href="#/p5.Graphics">p5.Graphics</a>, or
   * * a <a href="#/p5.Framebuffer">p5.Framebuffer</a>.
   *
   * The retrieved color that is returned will behave like a vec4, with components
   * for red, green, blue, and alpha, each between 0.0 and 1.0.
   *
   * Linear interpolation is used by default. For Framebuffer sources, you can
   * prevent this by creating the buffer with:
   * ```js
   * createFramebuffer({
   *     textureFiltering: NEAREST
   *  })
   * ```
   * This can be useful if you are using your texture to store data other than color.
   * See <a href="#/p5/createFramebuffer/">createFramebuffer</a>.
   *
   * Note: The `getTexture` function is only available when using p5.strands.
   *
   * @method getTexture
   * @beta
   *
   * @param texture The texture to sample from.
   * (e.g. a p5.Image, p5.Graphics, or p5.Framebuffer).
   *
   * @param coords The 2D coordinates to sample from.
   * This should be between [0,0] (the top-left) and [1,1] (the bottom-right)
   * of the texture.  It should be compatible with a vec2.
   *
   * @returns {*} The color of the given texture at the given coordinates.  This
   * will behave as a vec4 holding components r, g, b, and a (alpha), with each component being in the range 0.0 to 1.0.
   *
   * @example
   * <div modernizr='webgl'>
   * <code>
   * // A filter shader (using p5.strands) which will
   * // sample and invert the color of each pixel
   * // from the canvas.
   * function setup() {
   *   createCanvas(100, 100, WEBGL);
   *   let myShader = buildFilterShader(buildIt);
   *
   *   background("white");
   *   fill("red");
   *   circle(0, 0, 50);
   *
   *   filter(myShader); //Try commenting this out!
   *
   *   describe("A cyan circle on black background");
   * }
   *
   * function buildIt() {
   *   filterColor.begin();
   *
   *   //Sample the color of the pixel from the
   *   //canvas at the same coordinate.
   *   let c = getTexture(filterColor.canvasContent,
   *                      filterColor.texCoord);
   *
   *   //Make a new color by inverting r, g, and b
   *   let newColor = [1 - c.r, 1 - c.g, 1 - c.b, c.a];
   *
   *   //Finally, use it for this pixel!
   *   filterColor.set(newColor);
   *
   *   filterColor.end();
   * }
   * </code>
   *
   *
   * @example
   * <div modernizr='webgl'>
   * <code>
   * // This primitive edge-detection filter samples
   * // and compares the colors of the current pixel
   * // on the canvas, and a little to the right.
   * // It marks if they differ much.
   * let myShader;
   *
   * function setup() {
   *   createCanvas(100, 100, WEBGL);
   *   myShader = buildFilterShader(myShaderBuilder);
   *   describe("A rough partial outline of a square rotating around a circle");
   * }
   *
   * function draw() {
   *   drawADesign();
   *
   *   filter(myShader); // try commenting this out
   * }
   *
   * function myShaderBuilder() {
   *   filterColor.begin();
   *
   *   //The position of the current pixel...
   *   let coordHere = filterColor.texCoord;
   *   //and some small amount to the right.
   *   let coordRight = coordHere + [0.01, 0];
   *
   *   //The canvas content is a texture.
   *   let cnvTex = filterColor.canvasContent;
   *
   *   //Sample the colors from it at our two positions
   *   let colorHere = getTexture(cnvTex, coordHere);
   *   let colorRight = getTexture(cnvTex, coordRight);
   *
   *   // Calculate a (very rough) color difference.
   *   let difference = length(colorHere - colorRight);
   *
   *   //We'll use a black color by default...
   *   let resultColor = [0, 0, 0, 1];
   *   //or white if the samples were different.
   *   if (difference > 0.3) {
   *     resultColor = [1, 1, 1, 1];
   *   }
   *   filterColor.set(resultColor);
   *
   *   filterColor.end();
   * }
   *
   * //Draw a few shapes, just to test the filter with
   * function drawADesign() {
   *   background(50);
   *   noStroke();
   *   lights();
   *   sphere(20);
   *   rotate(frameCount / 300);
   *   square(0, 0, 30);
   * }
   * </code>
   * </div>
   */

  /**
   * @method getWorldInputs
   * @param {Function} callback
   */

  /**
   * @method getPixelInputs
   * @param {Function} callback
   */

  /**
   * @method getFinalColor
   * @param {Function} callback
   */

  /**
   * @method getColor
   * @param {Function} callback
   */

  /**
   * @method getObjectInputs
   * @param {Function} callback
   */

  /**
   * @method getCameraInputs
   * @param {Function} callback
   */

  /**
   * This file setup global mode automatic instantiation
   *
   * if sketch is on window
   * assume "global" mode
   * and instantiate p5 automatically
   * otherwise do nothing
   *
   * @private
   * @return {Undefined}
   */
  const _globalInit = () => {
    // Could have been any property defined within the p5 constructor.
    // If that property is already a part of the global object,
    // this code has already run before, likely due to a duplicate import
    if (typeof window._setupDone !== 'undefined') {
      console.warn(
        'p5.js seems to have been imported multiple times. Please remove the duplicate import'
      );
      return;
    }

    if (!window.mocha) {
      const p5ReadyEvent = new Event('p5Ready');
      window.dispatchEvent(p5ReadyEvent);

      // If there is a setup or draw function on the window
      // then instantiate p5 in "global" mode
      if (
        ((window.setup && typeof window.setup === 'function') ||
          (window.draw && typeof window.draw === 'function')) &&
        !p5$2.instance
      ) {
        new p5$2();
      }
    }
  };

  // make a promise that resolves when the document is ready
  const waitForDocumentReady = () =>
    new Promise((resolve, reject) => {
      // if the page is ready, initialize p5 immediately
      if (document.readyState === 'complete') {
        resolve();
        // if the page is still loading, add an event listener
        // and initialize p5 as soon as it finishes loading
      } else {
        window.addEventListener('load', resolve, false);
      }
    });

  // only load translations if we're using the full, un-minified library
  const waitingForTranslator =
    typeof IS_MINIFIED === 'undefined' ? initialize() :
      Promise.resolve();

  // core
  shape(p5$2);
  accessibility(p5$2);
  color(p5$2);
  friendlyErrors(p5$2);
  data(p5$2);
  dom(p5$2);
  events(p5$2);
  image(p5$2);
  io(p5$2);
  math(p5$2);
  utilities(p5$2);
  webgl(p5$2);
  type(p5$2);
  p5$2.registerAddon(shader);
  p5$2.registerAddon(strands);
  Promise.all([waitForDocumentReady(), waitingForTranslator]).then(_globalInit);

  return p5$2;

})();
