import { p as CORNER, t as CORNERS, v as CENTER, aB as COVER, aC as CONTAIN, a9 as RIGHT, aw as BOTTOM, B as BLEND, aD as FILL, ad as IMAGE, C as CLAMP, w as ROUND, Y as LINES, X as POINTS, c as TRIANGLES, ab as BLUR, D as DARKEST, L as LIGHTEST, A as ADD, S as SUBTRACT, a as SCREEN, E as EXCLUSION, R as REPLACE, M as MULTIPLY, b as REMOVE, as as BURN, ao as OVERLAY, ap as HARD_LIGHT, aq as SOFT_LIGHT, ar as DODGE, d as UNSIGNED_INT, U as UNSIGNED_BYTE, av as LEFT, ax as BASELINE, ay as TOP, aE as SIMPLE, aF as FULL, q as TWO_PI, Q as OPEN, a2 as NORMAL, V as CLOSE, at as PIE, au as CHORD, h as TEXTURE, P as P2D, g as LINEAR, aa as WEBGL2, N as NEAREST, aG as LINEAR_MIPMAP, f as REPEAT, e as MIRROR, F as FLOAT, ac as WEBGL, H as HALF_FLOAT } from './constants-BxjhKpTv.js';
import { C as Color, c as creatingReading, h as RGBA, R as RGB } from './creating_reading-C7hu6sg1.js';
import { Element } from './dom/p5.Element.js';
import { R as Renderer, I as Image } from './p5.Renderer-BSGddFv7.js';
import { MediaElement } from './dom/p5.MediaElement.js';
import primitives from './shape/2d_primitives.js';
import attributes from './shape/attributes.js';
import curves from './shape/curves.js';
import vertex from './shape/vertex.js';
import setting from './color/setting.js';
import * as omggif from 'omggif';
import canvas from './core/helpers.js';
import { parse } from './io/csv.js';
import { _checkFileExtension, downloadFile } from './io/utilities.js';
import { GIFEncoder, quantize, nearestColorIndex } from 'gifenc';
import pixels from './image/pixels.js';
import transform from './core/transform.js';
import GeometryBuilder from './webgl/GeometryBuilder.js';
import './math/p5.Matrix.js';
import { Vector } from './math/p5.Vector.js';
import { Quat } from './webgl/p5.Quat.js';
import { Matrix } from './math/Matrices/Matrix.js';
import { ShapeBuilder } from './webgl/ShapeBuilder.js';
import { GeometryBufferCache } from './webgl/GeometryBufferCache.js';
import { filterParamDefaults } from './image/const.js';
import customShapes, { PrimitiveToVerticesConverter } from './shape/custom_shapes.js';
import { DataArray } from './webgl/p5.DataArray.js';
import { textCoreConstants } from './type/textCore.js';
import { RenderBuffer } from './webgl/p5.RenderBuffer.js';
import { makeFilterShader } from './core/filterShaders.js';
import { getStrokeDefs } from './webgl/enums.js';
import { Geometry } from './webgl/p5.Geometry.js';
import { Shader } from './webgl/p5.Shader.js';
import trigonometry from './math/trigonometry.js';

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


function image(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) {
      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 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'
    });
    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(p5, p5.prototype);
}

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

/**
 * @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 = GIFEncoder();

    // 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] = nearestColorIndex(
            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 = quantize(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 omggif.GifReader(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 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);
}

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(this);
    }
    return this.filterLayer;
  }
  getFilterLayerTemp() {
    if (!this.filterLayerTemp) {
      this.filterLayerTemp = new Framebuffer(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(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(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) {
      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, textFont } = 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(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(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, p5.prototype);
}

/**
 * @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);
}

/**
 * @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);
   * }
   * ```
   *
   * We can use the `noise()` function built into strands to generate a color for each pixel.  (Again no need here for underlying content for the filter to operate on.)  Again we'll animate by passing in an announced uniform variable  `time` with `setUniform()`, each frame.
   *
   * ```js example
   * let myFilter;
   *
   * function setup() {
   *   createCanvas(100, 100, WEBGL);
   *   myFilter = buildFilterShader(noiseShaderCallback);
   *   describe('Evolving animated cloud-like noise in cyan and magenta');
   * }
   *
   * function noiseShaderCallback() {
   *   let time = uniformFloat();
   *   filterColor.begin();
   *   let coord = filterColor.texCoord;
   *
   *   //generate a value roughly between 0 and 1
   *   let noiseVal = noise(coord.x, coord.y, time / 2000);
   *
   *   let result = mix(
   *     [1, 0, 1, 1], // Magenta
   *     [0, 1, 1, 1], // Cyan
   *     noiseVal
   *   );
   *   filterColor.set(result);
   *   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.
   * @param {Object} [scope] An optional scope object passed to .modify().
   * @returns {p5.Shader} The material shader
   */
  /**
   * @method buildFilterShader
   * @param {Object} hooks An object specifying p5.strands hooks in GLSL.
   * @param {Object} [scope] An optional scope object passed to .modify().
   * @returns {p5.Shader} The material shader
   */
  fn.buildFilterShader = function (callback, scope) {
    return this.baseFilterShader().modify(callback, scope);
  };

  /**
   * 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.
   * @param {Object} [scope] An optional scope object passed to .modify().
   * @returns {p5.Shader} The material shader.
   */
  /**
   * @method buildMaterialShader
   * @param {Object} hooks An object specifying p5.strands hooks in GLSL.
   * @param {Object} [scope] An optional scope object passed to .modify().
   * @returns {p5.Shader} The material shader.
   */
  fn.buildMaterialShader = function (cb, scope) {
    return this.baseMaterialShader().modify(cb, scope);
  };

  /**
   * 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/buildFilterShader">`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.
   * @param {Object} [scope] An optional scope object passed to .modify().
   * @returns {p5.Shader} The normal shader.
   */
  /**
   * @method buildNormalShader
   * @param {Object} hooks An object specifying p5.strands hooks in GLSL.
   * @param {Object} [scope] An optional scope object passed to .modify().
   * @returns {p5.Shader} The normal shader.
   */
  fn.buildNormalShader = function (cb, scope) {
    return this.baseNormalShader().modify(cb, scope);
  };

  /**
   * 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.
   * @param {Object} [scope] An optional scope object passed to .modify().
   * @returns {p5.Shader} The color shader.
   */
  /**
   * @method buildColorShader
   * @param {Object} hooks An object specifying p5.strands hooks in GLSL.
   * @param {Object} [scope] An optional scope object passed to .modify().
   * @returns {p5.Shader} The color shader.
   */
  fn.buildColorShader = function (cb, scope) {
    return this.baseColorShader().modify(cb, scope);
  };

  /**
   * 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.
   * @param {Object} [scope] An optional scope object passed to .modify().
   * @returns {p5.Shader} The stroke shader.
   */
  /**
   * @method buildStrokeShader
   * @param {Object} hooks An object specifying p5.strands hooks in GLSL.
   * @param {Object} [scope] An optional scope object passed to .modify().
   * @returns {p5.Shader} The stroke shader.
   */
  fn.buildStrokeShader = function (cb, scope) {
    return this.baseStrokeShader().modify(cb, scope);
  };

  /**
   * 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 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(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(p5, p5.Graphics.prototype);
  pixels(p5, p5.Graphics.prototype);

  transform(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, p5.prototype);
}

/**
 * @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] };
  }
}

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;

  /**
   * 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, p5.prototype);
}

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

export { Camera as C, FramebufferTexture as F, Graphics as G, MipmapTexture as M, Renderer3D as R, Texture as T, request as a, loadingDisplaying as b, camera as c, files as d, setWebGLTextureParams as e, framebuffer as f, getWebGLUniformMetadata as g, getWebGLShaderAttributes as h, image as i, populateGLSLHooks as j, rendering as k, light as l, material as m, graphics as n, readPixelWebGL as o, primitives3D as p, readPixelsWebGL as q, renderer3D as r, setWebGLUniformValue as s, texture as t, checkWebGLCapabilities as u, FramebufferCamera as v, Framebuffer as w, renderers as x };
