import { P as P2D, ac as WEBGL, B as BLEND, al as _DEFAULT_FILL, am as _DEFAULT_STROKE, w as ROUND, b as REMOVE, S as SUBTRACT, D as DARKEST, L as LIGHTEST, an as DIFFERENCE, M as MULTIPLY, E as EXCLUSION, a as SCREEN, R as REPLACE, ao as OVERLAY, ap as HARD_LIGHT, aq as SOFT_LIGHT, ar as DODGE, as as BURN, A as ADD, at as PIE, au as CHORD, q as TWO_PI, x as SQUARE, y as PROJECT, z as BEVEL, I as MITER, a9 as RIGHT, v as CENTER, av as LEFT, aw as BOTTOM, ax as BASELINE, ay as TOP, Q as OPEN, az as VERSION, a3 as constants } from './constants-BxjhKpTv.js';
import transform from './core/transform.js';
import structure from './core/structure.js';
import environment from './core/environment.js';
import { G as Graphics, k as rendering, n as graphics } from './rendering-C9g7uSQ5.js';
import { R as Renderer, I as Image, r as renderer } from './p5.Renderer-BSGddFv7.js';
import { Element } from './dom/p5.Element.js';
import { MediaElement } from './dom/p5.MediaElement.js';
import { b as RGBHDR } from './creating_reading-C7hu6sg1.js';
import FilterRenderer2D from './image/filterRenderer2D.js';
import './math/p5.Matrix.js';
import { PrimitiveToPath2DConverter } from './shape/custom_shapes.js';
import { DefaultFill, textCoreConstants } from './type/textCore.js';
import { Matrix } from './math/Matrices/Matrix.js';

const styleEmpty = 'rgba(0,0,0,0)';
// const alphaThreshold = 0.00125; // minimum visible

class Renderer2D extends Renderer {
  constructor(pInst, w, h, isMainCanvas, elt, attributes = {}) {
    super(pInst, w, h, isMainCanvas);

    this.canvas = this.elt = elt || document.createElement('canvas');

    if (isMainCanvas) {
      // for pixel method sharing with pimage
      this._pInst._curElement = this;
      this._pInst.canvas = this.canvas;
    } else {
      // hide if offscreen buffer by default
      this.canvas.style.display = 'none';
    }

    this.elt.id = 'defaultCanvas0';
    this.elt.classList.add('p5Canvas');

    // Extend renderer with methods of p5.Element with getters
    for (const p of Object.getOwnPropertyNames(Element.prototype)) {
      if (p !== 'constructor' && p[0] !== '_') {
        Object.defineProperty(this, p, {
          get() {
            return this.wrappedElt[p];
          }
        });
      }
    }

    // Set canvas size
    this.elt.width = w * this._pixelDensity;
    this.elt.height = h * this._pixelDensity;
    this.elt.style.width = `${w}px`;
    this.elt.style.height = `${h}px`;

    // Attach canvas element to DOM
    if (this._pInst._userNode) {
      // user input node case
      this._pInst._userNode.appendChild(this.elt);
    } else {
      //create main element
      if (document.getElementsByTagName('main').length === 0) {
        let m = document.createElement('main');
        document.body.appendChild(m);
      }
      //append canvas to main
      document.getElementsByTagName('main')[0].appendChild(this.elt);
    }

    // Get and store drawing context
    this.drawingContext = this.canvas.getContext('2d', attributes);
    if(attributes.colorSpace === 'display-p3'){
      this.states.colorMode = RGBHDR;
    }
    this.scale(this._pixelDensity, this._pixelDensity);

    // Set and return p5.Element
    this.wrappedElt = new Element(this.elt, this._pInst);
    this.clipPath = null;
  }

  get filterRenderer() {
    if (!this._filterRenderer) {
      this._filterRenderer = new FilterRenderer2D(this);
    }
    return this._filterRenderer;
  }

  remove(){
    this.wrappedElt.remove();
    this.wrappedElt = null;
    this.canvas = null;
    this.elt = null;
  }

  getFilterGraphicsLayer() {
    // create hidden webgl renderer if it doesn't exist
    if (!this.filterGraphicsLayer) {
      const pInst = this._pInst;

      // create secondary layer
      this.filterGraphicsLayer =
        new Graphics(
          this.width,
          this.height,
          WEBGL,
          pInst
        );
    }
    if (
      this.filterGraphicsLayer.width !== this.width ||
      this.filterGraphicsLayer.height !== this.height
    ) {
      // Resize the graphics layer
      this.filterGraphicsLayer.resizeCanvas(this.width, this.height);
    }
    if (
      this.filterGraphicsLayer.pixelDensity() !== this._pInst.pixelDensity()
    ) {
      this.filterGraphicsLayer.pixelDensity(this._pInst.pixelDensity());
    }

    return this.filterGraphicsLayer;
  }

  _applyDefaults() {
    this.states.setValue('_cachedFillStyle', undefined);
    this.states.setValue('_cachedStrokeStyle', undefined);
    this._cachedBlendMode = BLEND;
    this._setFill(_DEFAULT_FILL);
    this._setStroke(_DEFAULT_STROKE);
    this.drawingContext.lineCap = ROUND;
    this.drawingContext.font = 'normal 12px sans-serif';
  }

  resize(w, h) {
    super.resize(w, h);

    // save canvas properties
    const props = {};
    for (const key in this.drawingContext) {
      const val = this.drawingContext[key];
      if (typeof val !== 'object' && typeof val !== 'function') {
        props[key] = val;
      }
    }

    this.canvas.width = w * this._pixelDensity;
    this.canvas.height = h * this._pixelDensity;
    this.canvas.style.width = `${w}px`;
    this.canvas.style.height = `${h}px`;
    this.drawingContext.scale(
      this._pixelDensity,
      this._pixelDensity
    );

    // reset canvas properties
    for (const savedKey in props) {
      try {
        this.drawingContext[savedKey] = props[savedKey];
      } catch (err) {
        // ignore read-only property errors
      }
    }
  }

  //////////////////////////////////////////////
  // COLOR | Setting
  //////////////////////////////////////////////

  background(...args) {
    this.push();
    this.resetMatrix();

    if (args[0] instanceof Image) {
      if (args[1] >= 0) {
        // set transparency of background
        const img = args[0];
        this.drawingContext.globalAlpha = args[1] / 255;
        this._pInst.image(img, 0, 0, this.width, this.height);
      } else {
        this._pInst.image(args[0], 0, 0, this.width, this.height);
      }
    } else {
      // create background rect
      const color = this._pInst.color(...args);

      // Add accessible outputs if the method exists; on success,
      // set the accessible output background to white.
      if (this._pInst._addAccsOutput?.()) {
        this._pInst._accsBackground?.(color._getRGBA([255, 255, 255, 255]));
      }

      const newFill = color.toString();
      this._setFill(newFill);

      if (this._isErasing) {
        this.blendMode(this._cachedBlendMode);
      }

      this.drawingContext.fillRect(0, 0, this.width, this.height);

      if (this._isErasing) {
        this._pInst.erase();
      }
    }
    this.pop();
  }

  clear() {
    this.drawingContext.save();
    this.resetMatrix();
    this.drawingContext.clearRect(0, 0, this.width, this.height);
    this.drawingContext.restore();
  }

  fill(...args) {
    super.fill(...args);
    const color = this.states.fillColor;
    this._setFill(color.toString());

    // Add accessible outputs if the method exists; on success,
    // set the accessible output background to white.
    if (this._pInst._addAccsOutput?.()) {
      this._pInst._accsCanvasColors?.('fill', color._getRGBA([255, 255, 255, 255]));
    }
  }

  stroke(...args) {
    super.stroke(...args);
    const color = this.states.strokeColor;
    this._setStroke(color.toString());

    // Add accessible outputs if the method exists; on success,
    // set the accessible output background to white.
    if (this._pInst._addAccsOutput?.()) {
      this._pInst._accsCanvasColors?.('stroke', color._getRGBA([255, 255, 255, 255]));
    }
  }

  erase(opacityFill, opacityStroke) {
    if (!this._isErasing) {
      // cache the fill style
      this.states.setValue('_cachedFillStyle', this.drawingContext.fillStyle);
      const newFill = this._pInst.color(255, opacityFill).toString();
      this.drawingContext.fillStyle = newFill;

      // cache the stroke style
      this.states.setValue('_cachedStrokeStyle', this.drawingContext.strokeStyle);
      const newStroke = this._pInst.color(255, opacityStroke).toString();
      this.drawingContext.strokeStyle = newStroke;

      // cache blendMode
      const tempBlendMode = this._cachedBlendMode;
      this.blendMode(REMOVE);
      this._cachedBlendMode = tempBlendMode;

      this._isErasing = true;
    }
  }

  noErase() {
    if (this._isErasing) {
      this.drawingContext.fillStyle = this.states._cachedFillStyle;
      this.drawingContext.strokeStyle = this.states._cachedStrokeStyle;

      this.blendMode(this._cachedBlendMode);
      this._isErasing = false;
    }
  }

  drawShape(shape) {
    const visitor = new PrimitiveToPath2DConverter({
      strokeWeight: this.states.strokeWeight
    });
    shape.accept(visitor);
    if (this._clipping) {
      const currentTransform = this.drawingContext.getTransform();
      const clipBaseTransform = this._clipBaseTransform.inverse();
      const relativeTransform = clipBaseTransform.multiply(currentTransform);
      this.clipPath.addPath(visitor.path, relativeTransform);
      this.clipPath.closePath();
    } else {
      if (this.states.fillColor) {
        this.drawingContext.fill(visitor.path);
      }
      if (this.states.strokeColor) {
        this.drawingContext.stroke(visitor.path);
      }
    }
  }

  beginClip(options = {}) {
    super.beginClip(options);
    this._clipBaseTransform = this.drawingContext.getTransform();
    // cache the fill style
    this.states.setValue('_cachedFillStyle', this.drawingContext.fillStyle);
    const newFill = this._pInst.color(255, 0).toString();
    this.drawingContext.fillStyle = newFill;

    // cache the stroke style
    this.states.setValue('_cachedStrokeStyle', this.drawingContext.strokeStyle);
    const newStroke = this._pInst.color(255, 0).toString();
    this.drawingContext.strokeStyle = newStroke;

    // cache blendMode
    const tempBlendMode = this._cachedBlendMode;
    this.blendMode(BLEND);
    this._cachedBlendMode = tempBlendMode;

    // Since everything must be in one path, create a new single Path2D to chain all shapes onto.
    // Start a new path. Everything from here on out should become part of this
    // one path so that we can clip to the whole thing.
    this.clipPath = new Path2D();
    this._clipBaseTransform = this.drawingContext.getTransform();

    if (this._clipInvert) {
      // Slight hack: draw a big rectangle over everything with reverse winding
      // order. This is hopefully large enough to cover most things.
      this.clipPath.moveTo(
        -2 * this.width,
        -2 * this.height
      );
      this.clipPath.lineTo(
        -2 * this.width,
        2 * this.height
      );
      this.clipPath.lineTo(
        2 * this.width,
        2 * this.height
      );
      this.clipPath.lineTo(
        2 * this.width,
        -2 * this.height
      );
      this.clipPath.closePath();
    }
  }

  endClip() {
    const savedTransform = this.drawingContext.getTransform();
    this.drawingContext.setTransform(this._clipBaseTransform);
    this.drawingContext.clip(this.clipPath);
    this.drawingContext.setTransform(savedTransform);

    this.clipPath = null;

    super.endClip();

    this.drawingContext.fillStyle = this.states._cachedFillStyle;
    this.drawingContext.strokeStyle = this.states._cachedStrokeStyle;

    this.blendMode(this._cachedBlendMode);
  }

  //////////////////////////////////////////////
  // IMAGE | Loading & Displaying
  //////////////////////////////////////////////

  image(
    img,
    sx,
    sy,
    sWidth,
    sHeight,
    dx,
    dy,
    dWidth,
    dHeight
  ) {
    let cnv;
    if (img.gifProperties) {
      img._animateGif(this._pInst);
    }

    try {
      if (img instanceof MediaElement) {
        img._ensureCanvas();
      }
      if (this.states.tint && img.canvas) {
        cnv = this._getTintedImageCanvas(img);
      }
      if (!cnv) {
        cnv = img.canvas || img.elt;
      }
      let s = 1;
      if (img.width && img.width > 0) {
        s = cnv.width / img.width;
      }
      if (this._isErasing) {
        this.blendMode(this._cachedBlendMode);
      }

      this.drawingContext.drawImage(
        cnv,
        s * sx,
        s * sy,
        s * sWidth,
        s * sHeight,
        dx,
        dy,
        dWidth,
        dHeight
      );
      if (this._isErasing) {
        this._pInst.erase();
      }
    } catch (e) {
      if (e.name !== 'NS_ERROR_NOT_AVAILABLE') {
        throw e;
      }
    }
  }

  _getTintedImageCanvas(img) {
    if (!img.canvas) {
      return img;
    }

    if (!img.tintCanvas) {
      // Once an image has been tinted, keep its tint canvas
      // around so we don't need to re-incur the cost of
      // creating a new one for each tint
      img.tintCanvas = document.createElement('canvas');
    }

    // Keep the size of the tint canvas up-to-date
    if (img.tintCanvas.width !== img.canvas.width) {
      img.tintCanvas.width = img.canvas.width;
    }
    if (img.tintCanvas.height !== img.canvas.height) {
      img.tintCanvas.height = img.canvas.height;
    }

    // Goal: multiply the r,g,b,a values of the source by
    // the r,g,b,a values of the tint color
    const ctx = img.tintCanvas.getContext('2d');

    ctx.save();
    ctx.clearRect(0, 0, img.canvas.width, img.canvas.height);

    if (
      this.states.tint[0] < 255 ||
      this.states.tint[1] < 255 ||
      this.states.tint[2] < 255
    ) {
      // Color tint: we need to use the multiply blend mode to change the colors.
      // However, the canvas implementation of this destroys the alpha channel of
      // the image. To accommodate, we first get a version of the image with full
      // opacity everywhere, tint using multiply, and then use the destination-in
      // blend mode to restore the alpha channel again.

      // Start with the original image
      ctx.drawImage(img.canvas, 0, 0);

      // This blend mode makes everything opaque but forces the luma to match
      // the original image again
      ctx.globalCompositeOperation = 'luminosity';
      ctx.drawImage(img.canvas, 0, 0);

      // This blend mode forces the hue and chroma to match the original image.
      // After this we should have the original again, but with full opacity.
      ctx.globalCompositeOperation = 'color';
      ctx.drawImage(img.canvas, 0, 0);

      // Apply color tint
      ctx.globalCompositeOperation = 'multiply';
      ctx.fillStyle = `rgb(${this.states.tint.slice(0, 3).join(', ')})`;
      ctx.fillRect(0, 0, img.canvas.width, img.canvas.height);

      // Replace the alpha channel with the original alpha * the alpha tint
      ctx.globalCompositeOperation = 'destination-in';
      ctx.globalAlpha = this.states.tint[3] / 255;
      ctx.drawImage(img.canvas, 0, 0);
    } else {
      // If we only need to change the alpha, we can skip all the extra work!
      ctx.globalAlpha = this.states.tint[3] / 255;
      ctx.drawImage(img.canvas, 0, 0);
    }

    ctx.restore();
    return img.tintCanvas;
  }

  //////////////////////////////////////////////
  // IMAGE | Pixels
  //////////////////////////////////////////////

  blendMode(mode) {
    if (mode === SUBTRACT) {
      console.warn('blendMode(SUBTRACT) only works in WEBGL mode.');
    } else if (
      mode === BLEND ||
      mode === REMOVE ||
      mode === DARKEST ||
      mode === LIGHTEST ||
      mode === DIFFERENCE ||
      mode === MULTIPLY ||
      mode === EXCLUSION ||
      mode === SCREEN ||
      mode === REPLACE ||
      mode === OVERLAY ||
      mode === HARD_LIGHT ||
      mode === SOFT_LIGHT ||
      mode === DODGE ||
      mode === BURN ||
      mode === ADD
    ) {
      this._cachedBlendMode = mode;
      this.drawingContext.globalCompositeOperation = mode;
    } else {
      throw new Error(`Mode ${mode} not recognized.`);
    }
  }

  blend(...args) {
    const currBlend = this.drawingContext.globalCompositeOperation;
    const blendMode = args[args.length - 1];

    const copyArgs = Array.prototype.slice.call(args, 0, args.length - 1);

    this.drawingContext.globalCompositeOperation = blendMode;

    p5.prototype.copy.apply(this, copyArgs);

    this.drawingContext.globalCompositeOperation = currBlend;
  }

  // p5.Renderer2D.prototype.get = p5.Renderer.prototype.get;
  // .get() is not overridden

  // x,y are canvas-relative (pre-scaled by _pixelDensity)
  _getPixel(x, y) {
    let imageData, index;
    imageData = this.drawingContext.getImageData(x, y, 1, 1).data;
    index = 0;
    return [
      imageData[index + 0],
      imageData[index + 1],
      imageData[index + 2],
      imageData[index + 3]
    ];
  }

  loadPixels() {
    const pd = this._pixelDensity;
    const w = this.width * pd;
    const h = this.height * pd;
    const imageData = this.drawingContext.getImageData(0, 0, w, h);
    // @todo this should actually set pixels per object, so diff buffers can
    // have diff pixel arrays.
    this.imageData = imageData;
    this.pixels = imageData.data;
  }

  set(x, y, imgOrCol) {
    // round down to get integer numbers
    x = Math.floor(x);
    y = Math.floor(y);
    if (imgOrCol instanceof Graphics || imgOrCol instanceof Image) {
      this.drawingContext.save();
      this.drawingContext.setTransform(1, 0, 0, 1, 0, 0);
      this.drawingContext.scale(
        this._pixelDensity,
        this._pixelDensity
      );
      const width = imgOrCol.width;
      const height = imgOrCol.height;
      this.drawingContext.clearRect(x, y, width, height);
      this.drawingContext.drawImage(imgOrCol.canvas, x, y, width, height);
    } else {
      let r = 0,
        g = 0,
        b = 0,
        a = 0;
      let idx =
        4 *
        (y *
          this._pixelDensity *
          (this.width * this._pixelDensity) +
          x * this._pixelDensity);
      if (!this.imageData) {
        this.loadPixels();
      }
      if (typeof imgOrCol === 'number') {
        if (idx < this.pixels.length) {
          r = imgOrCol;
          g = imgOrCol;
          b = imgOrCol;
          a = 255;
          //this.updatePixels.call(this);
        }
      } else if (Array.isArray(imgOrCol)) {
        if (imgOrCol.length < 4) {
          throw new Error('pixel array must be of the form [R, G, B, A]');
        }
        if (idx < this.pixels.length) {
          r = imgOrCol[0];
          g = imgOrCol[1];
          b = imgOrCol[2];
          a = imgOrCol[3];
          //this.updatePixels.call(this);
        }
      } else if (imgOrCol instanceof p5.Color) {
        if (idx < this.pixels.length) {
          [r, g, b, a] = imgOrCol._getRGBA([255, 255, 255, 255]);
          //this.updatePixels.call(this);
        }
      }
      // loop over pixelDensity * pixelDensity
      for (let i = 0; i < this._pixelDensity; i++) {
        for (let j = 0; j < this._pixelDensity; j++) {
          // loop over
          idx =
            4 *
            ((y * this._pixelDensity + j) *
              this.width *
              this._pixelDensity +
              (x * this._pixelDensity + i));
          this.pixels[idx] = r;
          this.pixels[idx + 1] = g;
          this.pixels[idx + 2] = b;
          this.pixels[idx + 3] = a;
        }
      }
    }
  }

  updatePixels(x, y, w, h) {
    const pd = this._pixelDensity;
    if (
      x === undefined &&
      y === undefined &&
      w === undefined &&
      h === undefined
    ) {
      x = 0;
      y = 0;
      w = this.width;
      h = this.height;
    }
    x *= pd;
    y *= pd;
    w *= pd;
    h *= pd;

    if (this.gifProperties) {
      this.gifProperties.frames[this.gifProperties.displayIndex].image =
        this.imageData;
    }

    this.drawingContext.putImageData(this.imageData, 0, 0, x, y, w, h);
  }

  //////////////////////////////////////////////
  // SHAPE | 2D Primitives
  //////////////////////////////////////////////

  /*
   * This function requires that:
   *
   *   0 <= start < TWO_PI
   *
   *   start <= stop < start + TWO_PI
   */
  arc(x, y, w, h, start, stop, mode) {
    const ctx = this.drawingContext;

    const centerX = x + w / 2,
      centerY = y + h / 2,
      radiusX = w / 2,
      radiusY = h / 2;
    if (this._clipping) {
      const tempPath = new Path2D();
      tempPath.ellipse(centerX, centerY, radiusX, radiusY, 0, start, stop);
      const currentTransform = this.drawingContext.getTransform();
      const clipBaseTransform = this._clipBaseTransform.inverse();
      const relativeTransform = clipBaseTransform.multiply(currentTransform);
      this.clipPath.addPath(tempPath, relativeTransform);
      return this;
    }
    // Determines whether to add a line to the center, which should be done
    // when the mode is PIE or default; as well as when the start and end
    // angles do not form a full circle.
    const createPieSlice = ! (
      mode === CHORD ||
      mode === OPEN ||
      (stop - start) % TWO_PI === 0
    );

    // Fill curves
    if (this.states.fillColor) {
      ctx.beginPath();
      ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, start, stop);
      if (createPieSlice) ctx.lineTo(centerX, centerY);
      ctx.closePath();
      ctx.fill();
    }

    // Stroke curves
    if (this.states.strokeColor) {
      ctx.beginPath();
      ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, start, stop);

      if (mode === PIE && createPieSlice) {
        // In PIE mode, stroke is added to the center and back to path,
        // unless the pie forms a complete ellipse (see: createPieSlice)
        ctx.lineTo(centerX, centerY);
      }

      if (mode === PIE || mode === CHORD) {
        // Stroke connects back to path begin for both PIE and CHORD
        ctx.closePath();
      }
      ctx.stroke();
    }

    return this;

  }

  ellipse(args) {
    const ctx = this.drawingContext;
    const doFill = !!this.states.fillColor,
      doStroke = this.states.strokeColor;
    const x = parseFloat(args[0]),
      y = parseFloat(args[1]),
      w = parseFloat(args[2]),
      h = parseFloat(args[3]);
    if (doFill && !doStroke) {
      if (this._getFill() === styleEmpty) {
        return this;
      }
    } else if (!doFill && doStroke) {
      if (this._getStroke() === styleEmpty) {
        return this;
      }
    }
    const centerX = x + w / 2,
      centerY = y + h / 2,
      radiusX = w / 2,
      radiusY = h / 2;
    if (this._clipping) {
      const tempPath = new Path2D();
      tempPath.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI);
      const currentTransform = this.drawingContext.getTransform();
      const clipBaseTransform = this._clipBaseTransform.inverse();
      const relativeTransform = clipBaseTransform.multiply(currentTransform);
      this.clipPath.addPath(tempPath, relativeTransform);
      return this;
    }
    ctx.beginPath();
    ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI);
    ctx.closePath();
    if (doFill) {
      ctx.fill();
    }
    if (doStroke) {
      ctx.stroke();
    }

    return this;
  }

  line(x1, y1, x2, y2) {
    const ctx = this.drawingContext;
    if (!this.states.strokeColor) {
      return this;
    } else if (this._getStroke() === styleEmpty) {
      return this;
    }
    if (this._clipping) {
      const tempPath = new Path2D();
      tempPath.moveTo(x1, y1);
      tempPath.lineTo(x2, y2);
      const currentTransform = this.drawingContext.getTransform();
      const clipBaseTransform = this._clipBaseTransform.inverse();
      const relativeTransform = clipBaseTransform.multiply(currentTransform);
      this.clipPath.addPath(tempPath, relativeTransform);
      return this;
    }
    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.stroke();

    return this;
  }

  point(x, y) {
    const ctx = this.drawingContext;
    if (!this.states.strokeColor) {
      return this;
    } else if (this._getStroke() === styleEmpty) {
      return this;
    }
    const s = this._getStroke();
    const f = this._getFill();
    if (this._clipping) {
      const tempPath = new Path2D();
      const drawingContextWidth = this.drawingContext.lineWidth;
      tempPath.arc(x, y, drawingContextWidth / 2, 0, TWO_PI);
      const currentTransform = this.drawingContext.getTransform();
      const clipBaseTransform = this._clipBaseTransform.inverse();
      const relativeTransform = clipBaseTransform.multiply(currentTransform);
      this.clipPath.addPath(tempPath, relativeTransform);
      return this;
    }
    this._setFill(s);
    ctx.beginPath();
    ctx.arc(x, y, ctx.lineWidth / 2, 0, TWO_PI, false);
    ctx.fill();
    this._setFill(f);

    return this;
  }

  quad(x1, y1, x2, y2, x3, y3, x4, y4) {
    const ctx = this.drawingContext;
    const doFill = !!this.states.fillColor,
      doStroke = this.states.strokeColor;
    if (doFill && !doStroke) {
      if (this._getFill() === styleEmpty) {
        return this;
      }
    } else if (!doFill && doStroke) {
      if (this._getStroke() === styleEmpty) {
        return this;
      }
    }
    if (this._clipping) {
      const tempPath = new Path2D();
      tempPath.moveTo(x1, y1);
      tempPath.lineTo(x2, y2);
      tempPath.lineTo(x3, y3);
      tempPath.lineTo(x4, y4);
      tempPath.closePath();
      const currentTransform = this.drawingContext.getTransform();
      const clipBaseTransform = this._clipBaseTransform.inverse();
      const relativeTransform = clipBaseTransform.multiply(currentTransform);
      this.clipPath.addPath(tempPath, relativeTransform);
      return this;
    }
    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.lineTo(x3, y3);
    ctx.lineTo(x4, y4);
    ctx.closePath();
    if (doFill) {
      ctx.fill();
    }
    if (doStroke) {
      ctx.stroke();
    }
    return this;
  }

  rect(args) {
    const x = args[0];
    const y = args[1];
    const w = args[2];
    const h = args[3];
    let tl = args[4];
    let tr = args[5];
    let br = args[6];
    let bl = args[7];
    const ctx = this.drawingContext;
    const doFill = !!this.states.fillColor,
      doStroke = this.states.strokeColor;
    if (doFill && !doStroke) {
      if (this._getFill() === styleEmpty) {
        return this;
      }
    } else if (!doFill && doStroke) {
      if (this._getStroke() === styleEmpty) {
        return this;
      }
    }
    if (this._clipping) {
      const tempPath = new Path2D();
      if (typeof tl === 'undefined') {
        tempPath.rect(x, y, w, h);
      } else {
        tempPath.roundRect(x, y, w, h, [tl, tr, br, bl]);
      }
      const currentTransform = this.drawingContext.getTransform();
      const clipBaseTransform = this._clipBaseTransform.inverse();
      const relativeTransform = clipBaseTransform.multiply(currentTransform);
      this.clipPath.addPath(tempPath, relativeTransform);
      return this;
    }
    ctx.beginPath();
    if (typeof tl === 'undefined') {
      // No rounded corners
      ctx.rect(x, y, w, h);
    } else {
      // At least one rounded corner
      // Set defaults when not specified
      if (typeof tr === 'undefined') {
        tr = tl;
      }
      if (typeof br === 'undefined') {
        br = tr;
      }
      if (typeof bl === 'undefined') {
        bl = br;
      }

      // corner rounding must always be positive
      const absW = Math.abs(w);
      const absH = Math.abs(h);
      const hw = absW / 2;
      const hh = absH / 2;

      // Clip radii
      if (absW < 2 * tl) {
        tl = hw;
      }
      if (absH < 2 * tl) {
        tl = hh;
      }
      if (absW < 2 * tr) {
        tr = hw;
      }
      if (absH < 2 * tr) {
        tr = hh;
      }
      if (absW < 2 * br) {
        br = hw;
      }
      if (absH < 2 * br) {
        br = hh;
      }
      if (absW < 2 * bl) {
        bl = hw;
      }
      if (absH < 2 * bl) {
        bl = hh;
      }

      ctx.roundRect(x, y, w, h, [tl, tr, br, bl]);
    }
    if (doFill) {
      ctx.fill();
    }
    if (doStroke) {
      ctx.stroke();
    }
    return this;
  }


  triangle(args) {
    const ctx = this.drawingContext;
    const doFill = !!this.states.fillColor,
      doStroke = this.states.strokeColor;
    const x1 = args[0],
      y1 = args[1];
    const x2 = args[2],
      y2 = args[3];
    const x3 = args[4],
      y3 = args[5];
    if (doFill && !doStroke) {
      if (this._getFill() === styleEmpty) {
        return this;
      }
    } else if (!doFill && doStroke) {
      if (this._getStroke() === styleEmpty) {
        return this;
      }
    }
    if (this._clipping) {
      const tempPath = new Path2D();
      tempPath.moveTo(x1, y1);
      tempPath.lineTo(x2, y2);
      tempPath.lineTo(x3, y3);
      tempPath.closePath();
      const currentTransform = this.drawingContext.getTransform();
      const clipBaseTransform = this._clipBaseTransform.inverse();
      const relativeTransform = clipBaseTransform.multiply(currentTransform);
      this.clipPath.addPath(tempPath, relativeTransform);
      return this;
    }
    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.lineTo(x3, y3);
    ctx.closePath();
    if (doFill) {
      ctx.fill();
    }
    if (doStroke) {
      ctx.stroke();
    }

    return this;
  }

  //////////////////////////////////////////////
  // SHAPE | Attributes
  //////////////////////////////////////////////

  strokeCap(cap) {
    if (
      cap === ROUND ||
      cap === SQUARE ||
      cap === PROJECT
    ) {
      this.drawingContext.lineCap = cap;
    }
    return this;
  }

  strokeJoin(join) {
    if (
      join === ROUND ||
      join === BEVEL ||
      join === MITER
    ) {
      this.drawingContext.lineJoin = join;
    }
    return this;
  }

  strokeWeight(w) {
    super.strokeWeight(w);
    if (typeof w === 'undefined' || w === 0) {
      // hack because lineWidth 0 doesn't work
      this.drawingContext.lineWidth = 0.0001;
    } else {
      this.drawingContext.lineWidth = w;
    }
    return this;
  }

  _getFill() {
    if (!this.states._cachedFillStyle) {
      this.states.setValue('_cachedFillStyle', this.drawingContext.fillStyle);
    }
    return this.states._cachedFillStyle;
  }

  _setFill(fillStyle) {
    if (fillStyle !== this.states._cachedFillStyle) {
      this.drawingContext.fillStyle = fillStyle;
      this.states.setValue('_cachedFillStyle', fillStyle);
    }
  }

  _getStroke() {
    if (!this.states._cachedStrokeStyle) {
      this.states.setValue('_cachedStrokeStyle', this.drawingContext.strokeStyle);
    }
    return this.states._cachedStrokeStyle;
  }

  _setStroke(strokeStyle) {
    if (strokeStyle !== this.states._cachedStrokeStyle) {
      this.drawingContext.strokeStyle = strokeStyle;
      this.states.setValue('_cachedStrokeStyle', strokeStyle);
    }
  }

  //////////////////////////////////////////////
  // TRANSFORM
  //////////////////////////////////////////////

  applyMatrix(a, b, c, d, e, f) {
    this.drawingContext.transform(a, b, c, d, e, f);
  }

  getWorldToScreenMatrix() {
    let domMatrix = new DOMMatrix()
      .scale(1 / this._pixelDensity)
      .multiply(this.drawingContext.getTransform());
    return new Matrix(domMatrix.toFloat32Array());
  }

  resetMatrix() {
    this.drawingContext.setTransform(1, 0, 0, 1, 0, 0);
    this.drawingContext.scale(
      this._pixelDensity,
      this._pixelDensity
    );
    return this;
  }

  rotate(rad) {
    this.drawingContext.rotate(rad);
  }

  scale(x, y) {
    this.drawingContext.scale(x, y);
    return this;
  }

  translate(x, y) {
    // support passing a vector as the 1st parameter
    if (x instanceof p5.Vector) {
      y = x.y;
      x = x.x;
    }
    this.drawingContext.translate(x, y);
    return this;
  }

  //////////////////////////////////////////////
  // TYPOGRAPHY (see src/type/textCore.js)
  //////////////////////////////////////////////

  //////////////////////////////////////////////
  // STRUCTURE
  //////////////////////////////////////////////

  // a push() operation is in progress.
  // the renderer should return a 'style' object that it wishes to
  // store on the push stack.
  // derived renderers should call the base class' push() method
  // to fetch the base style object.
  push() {
    this.drawingContext.save();

    // get the base renderer style
    return super.push();
  }

  // a pop() operation is in progress
  // the renderer is passed the 'style' object that it returned
  // from its push() method.
  // derived renderers should pass this object to their base
  // class' pop method
  pop(style) {
    this.drawingContext.restore();

    super.pop(style);
  }

  // Text support methods
  textCanvas() {
    return this.canvas;
  }

  textDrawingContext() {
    return this.drawingContext;
  }

  _renderText(text, x, y, maxY, minY) {
    let states = this.states;
    let context = this.textDrawingContext();

    if (y < minY || y >= maxY) {
      return; // don't render lines beyond minY/maxY
    }

    this.push();

    // no stroke unless specified by user
    if (states.strokeColor && states.strokeSet) {
      context.strokeText(text, x, y);
    }

    if (!this._clipping && states.fillColor) {

      // if fill hasn't been set by user, use default text fill
      if (!states.fillSet) {
        this._setFill(DefaultFill);
      }
      context.fillText(text, x, y);
    }

    this.pop();
  }

  /*
    Position the lines of text based on their textAlign/textBaseline properties
  */
  _positionLines(x, y, width, height, lines) {
    let { textLeading, textAlign } = this.states;
    let adjustedX, lineData = new Array(lines.length);
    let adjustedW = typeof width === 'undefined' ? 0 : width;
    let adjustedH = typeof height === 'undefined' ? 0 : height;

    for (let i = 0; i < lines.length; i++) {
      switch (textAlign) {
        case textCoreConstants.START:
          throw new Error('textBounds: START not yet supported for textAlign'); // default to LEFT
        case LEFT:
          adjustedX = x;
          break;
        case CENTER:
          adjustedX = x + adjustedW / 2;
          break;
        case RIGHT:
          adjustedX = x + adjustedW;
          break;
        case textCoreConstants.END:
          throw new Error('textBounds: END not yet supported for textAlign');
      }
      lineData[i] = { text: lines[i], x: adjustedX, y: y + i * textLeading };
    }

    return this._yAlignOffset(lineData, adjustedH);
  }

  /*
    Get the y-offset for text given the height, leading, line-count and textBaseline property
  */
  _yAlignOffset(dataArr, height) {
    if (typeof height === 'undefined') {
      throw Error('_yAlignOffset: height is required');
    }

    let { textLeading, textBaseline } = this.states;
    let yOff = 0, numLines = dataArr.length;
    let ydiff = height - (textLeading * (numLines - 1));

    switch (textBaseline) { // drawingContext ?
      case TOP:
        break; // ??
      case BASELINE:
        break;
      case textCoreConstants._CTX_MIDDLE:
        yOff = ydiff / 2 + this._middleAlignOffset();
        break;
      case BOTTOM:
        yOff = ydiff;
        break;
      case textCoreConstants.IDEOGRAPHIC:
        console.warn('textBounds: IDEOGRAPHIC not yet supported for textBaseline'); // FES?
        break;
      case textCoreConstants.HANGING:
        console.warn('textBounds: HANGING not yet supported for textBaseline'); // FES?
        break;
    }

    dataArr.forEach(ele => ele.y += yOff);
    return dataArr;
  }
}

function renderer2D(p5, fn){
  /**
   * p5.Renderer2D
   * The 2D graphics canvas renderer class.
   * extends p5.Renderer
   * @private
   */
  p5.Renderer2D = Renderer2D;
  p5.renderers[P2D] = Renderer2D;
  p5.renderers['p2d-hdr'] = new Proxy(Renderer2D, {
    construct(target, [pInst, w, h, isMainCanvas, elt]){
      return new target(pInst, w, h, isMainCanvas, elt, { colorSpace: 'display-p3' });
    }
  });
}

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


/**
 * This is the p5 instance constructor.
 *
 * A p5 instance holds all the properties and methods related to
 * a p5 sketch.  It expects an incoming sketch closure and it can also
 * take an optional node parameter for attaching the generated p5 canvas
 * to a node.  The sketch closure takes the newly created p5 instance as
 * its sole argument and may optionally set an asynchronous function
 * using `async/await`, along with the standard <a href="#/p5/setup">setup()</a>,
 *  and/or <a href="#/p5/setup">setup()</a>, and/or <a href="#/p5/draw">draw()</a>
 *  properties on it for running a sketch.
 *
 * A p5 sketch can run in "global" or "instance" mode:
 * "global"   - all properties and methods are attached to the window
 * "instance" - all properties and methods are bound to this p5 object
 *
 * @class p5
 * @param  {function(p5)}       sketch a closure that can set optional <a href="#/p5/preload">preload()</a>,
 *                              <a href="#/p5/setup">setup()</a>, and/or <a href="#/p5/draw">draw()</a> properties on the
 *                              given p5 instance
 * @param  {String|HTMLElement}        [node] element to attach canvas to
 * @return {p5}                 a p5 instance
 */
class p5 {
  static VERSION = VERSION;
  // This is a pointer to our global mode p5 instance, if we're in
  // global mode.
  static instance = null;
  static lifecycleHooks = {
    presetup: [],
    postsetup: [],
    predraw: [],
    postdraw: [],
    remove: []
  };

  // FES stub
  static _checkForUserDefinedFunctions = () => {};
  static _friendlyFileLoadError = () => {};

  constructor(sketch, node) {
    // Apply addon defined decorations
    if(p5.decorations.size > 0){
      for (const [patternArray, decoration] of p5.decorations) {
        for(const member in p5.prototype) {
          // Member must be a function
          if (typeof p5.prototype[member] !== 'function') continue;

          if (!patternArray.some(pattern => {
            if (typeof pattern === 'string') {
              return pattern === member;
            } else if (pattern instanceof RegExp) {
              return pattern.test(member);
            }
          })) continue;

          p5.prototype[member] = decoration(p5.prototype[member], {
            kind: 'method',
            name: member,
            access: {},
            static: false,
            private: false,
            addInitializer(initializer){}
          });
        }
      }

      p5.decorations.clear();
    }

    //////////////////////////////////////////////
    // PRIVATE p5 PROPERTIES AND METHODS
    //////////////////////////////////////////////

    this.hitCriticalError = false;
    this._setupDone = false;
    this._userNode = node;
    this._curElement = null;
    this._elements = [];
    this._glAttributes = null;
    this._webgpuAttributes = null;
    this._requestAnimId = 0;
    this._isGlobal = false;
    this._loop = true;
    this._startListener = null;
    this._initializeInstanceVariables();
    this._events = {
    };
    this._removeAbortController = new AbortController();
    this._removeSignal = this._removeAbortController.signal;
    this._millisStart = -1;
    this._recording = false;

    // States used in the custom random generators
    this._lcg_random_state = null; // NOTE: move to random.js
    this._gaussian_previous = false; // NOTE: move to random.js

    // ensure correct reporting of window dimensions
    this._updateWindowSize();

    const bindGlobal = createBindGlobal(this);
    // If the user has created a global setup or draw function,
    // assume "global" mode and make everything global (i.e. on the window)
    if (!sketch) {
      this._isGlobal = true;
      if (window.hitCriticalError) {
        return;
      }
      p5.instance = this;

      // Loop through methods on the prototype and attach them to the window
      // All methods and properties with name starting with '_' will be skipped
      for (const p of Object.getOwnPropertyNames(p5.prototype)) {
        if(p[0] === '_') continue;
        bindGlobal(p);
      }

      const protectedProperties = ['constructor', 'length'];
      // Attach its properties to the window
      for (const p in this) {
        if (this.hasOwnProperty(p)) {
          if(p[0] === '_' || protectedProperties.includes(p)) continue;
          bindGlobal(p);
        }
      }
    } else {
      // Else, the user has passed in a sketch closure that may set
      // user-provided 'setup', 'draw', etc. properties on this instance of p5
      sketch(this);

      // Run a check to see if the user has misspelled 'setup', 'draw', etc
      // detects capitalization mistakes only ( Setup, SETUP, MouseClicked, etc)
      p5._checkForUserDefinedFunctions(this);
    }

    const focusHandler = () => {
      this.focused = true;
    };
    const blurHandler = () => {
      this.focused = false;
    };
    window.addEventListener('focus', focusHandler);
    window.addEventListener('blur', blurHandler);
    p5.lifecycleHooks.remove.push(function() {
      window.removeEventListener('focus', focusHandler);
      window.removeEventListener('blur', blurHandler);
    });

    // Initialization complete, start runtime
    if (document.readyState === 'complete') {
      this.#_start();
    } else {
      this._startListener = this.#_start.bind(this);
      window.addEventListener('load', this._startListener, false);
    }
  }

  get pixels(){
    return this._renderer.pixels;
  }

  get drawingContext(){
    return this._renderer.drawingContext;
  }

  static _registeredAddons = new Set();
  static registerAddon(addon) {
    const lifecycles = {};

    // Don't re-register an addon. This allows addons
    // to register dependency addons without worrying about
    // them getting double-added.
    if (p5._registeredAddons.has(addon)) return;
    p5._registeredAddons.add(addon);

    addon(p5, p5.prototype, lifecycles);

    const validLifecycles = Object.keys(p5.lifecycleHooks);
    for(const name of validLifecycles){
      if(typeof lifecycles[name] === 'function'){
        p5.lifecycleHooks[name].push(lifecycles[name]);
      }
    }
  }

  static decorations = new Map();
  static decorateHelper(pattern, decoration){
    let patternArray = pattern;
    if (!Array.isArray(pattern)) patternArray = [pattern];
    p5.decorations.set(patternArray, decoration);
  }

  #customActions = {};
  _customActions = new Proxy({}, {
    get: (target, prop) => {
      if(!this.#customActions[prop]){
        const context = this._isGlobal ? window : this;
        if(typeof context[prop] === 'function'){
          this.#customActions[prop] = context[prop].bind(this);
        }
      }

      return this.#customActions[prop];
    }
  });

  async #_start() {
    if (this.hitCriticalError) return;
    // Find node if id given
    if (this._userNode) {
      if (typeof this._userNode === 'string') {
        this._userNode = document.getElementById(this._userNode);
      }
    }

    await this.#_setup();
    if (this.hitCriticalError) return;
    if (!this._recording) {
      this._draw();
    }
  }

  async #_setup() {
    // Run `presetup` hooks
    await this._runLifecycleHook('presetup');
    if (this.hitCriticalError) return;

    // Always create a default canvas.
    // Later on if the user calls createCanvas, this default one
    // will be replaced
    this.createCanvas(
      100,
      100,
      P2D
    );

    // Record the time when setup starts. millis() will start at 0 within
    // setup, but this isn't documented, locked-in behavior yet.
    this._millisStart = window.performance.now();

    const context = this._isGlobal ? window : this;
    if (typeof context.setup === 'function') {
      await context.setup();
    }
    if (this.hitCriticalError) return;

    const canvases = document.getElementsByTagName('canvas');
    for (const k of canvases) {
      // Apply touchAction = 'none' to canvases to prevent scrolling
      // when dragging on canvas elements
      k.style.touchAction = 'none';

      // unhide any hidden canvases that were created
      if (k.dataset.hidden === 'true') {
        k.style.visibility = '';
        delete k.dataset.hidden;
      }
    }

    this._lastTargetFrameTime = window.performance.now();
    this._lastRealFrameTime = window.performance.now();
    this._setupDone = true;
    if (this._accessibleOutputs.grid || this._accessibleOutputs.text) {
      this._updateAccsOutput();
    }

    // Run `postsetup` hooks
    await this._runLifecycleHook('postsetup');

    // Record the time when the draw loop starts so that millis() starts at 0
    // when the draw loop begins.
    this._millisStart = window.performance.now();
  }

  // While '#_draw' here is async, it is not awaited as 'requestAnimationFrame'
  // does not await its callback. Thus it is not recommended for 'draw()` to be
  // async and use await within as the next frame may start rendering before the
  // current frame finish awaiting. The same goes for lifecycle hooks 'predraw'
  // and 'postdraw'.
  async _draw(requestAnimationFrameTimestamp) {
    if (this.hitCriticalError) return;
    const now = requestAnimationFrameTimestamp || window.performance.now();
    const timeSinceLastFrame = now - this._lastTargetFrameTime;
    const targetTimeBetweenFrames = 1000 / this._targetFrameRate;

    // only draw if we really need to; don't overextend the browser.
    // draw if we're within 5ms of when our next frame should paint
    // (this will prevent us from giving up opportunities to draw
    // again when it's really about time for us to do so). fixes an
    // issue where the frameRate is too low if our refresh loop isn't
    // in sync with the browser. note that we have to draw once even
    // if looping is off, so we bypass the time delay if that
    // is the case.
    const epsilon = 5;
    if (
      !this._loop ||
      timeSinceLastFrame >= targetTimeBetweenFrames - epsilon
    ) {
      //mandatory update values(matrixes and stack)
      this.deltaTime = now - this._lastRealFrameTime;
      this._frameRate = 1000.0 / this.deltaTime;
      await this.redraw();
      this._lastTargetFrameTime = Math.max(this._lastTargetFrameTime
        + targetTimeBetweenFrames, now);
      this._lastRealFrameTime = now;

      // If the user is actually using mouse module, then update
      // coordinates, otherwise skip. We can test this by simply
      // checking if any of the mouse functions are available or not.
      // NOTE : This reflects only in complete build or modular build.
      if (typeof this._updateMouseCoords !== 'undefined') {
        this._updateMouseCoords();

        //reset delta values so they reset even if there is no mouse event to set them
        // for example if the mouse is outside the screen
        this.movedX = 0;
        this.movedY = 0;
      }
    }

    // get notified the next time the browser gives us
    // an opportunity to draw.
    if (this._loop) {
      this._requestAnimId = window.requestAnimationFrame(
        this._draw.bind(this)
      );
    }
  }

  /**
   * Removes the sketch from the web page.
   *
   * Calling `remove()` stops the draw loop and removes any HTML elements
   * created by the sketch, including the canvas. A new sketch can be
   * created by using the <a href="#/p5/p5">p5()</a> constructor, as in
   * `new p5()`.
   *
   * @example
   * // Double-click to remove the canvas.
   *
   * function setup() {
   *   createCanvas(100, 100);
   *
   *   describe(
   *     'A white circle on a gray background. The circle follows the mouse as the user moves. The sketch disappears when the user double-clicks.'
   *   );
   * }
   *
   * function draw() {
   *   // Paint the background repeatedly.
   *   background(200);
   *
   *   // Draw circles repeatedly.
   *   circle(mouseX, mouseY, 40);
   * }
   *
   * // Remove the sketch when the user double-clicks.
   * function doubleClicked() {
   *   remove();
   * }
   */
  async remove() {
    // Remove start listener to prevent orphan canvas being created
    if(this._startListener){
      window.removeEventListener('load', this._startListener, false);
    }

    if (this._curElement) {
      // stop draw
      this._loop = false;
      if (this._requestAnimId) {
        window.cancelAnimationFrame(this._requestAnimId);
      }

      // Send sketch remove signal
      this._removeAbortController.abort();

      // remove DOM elements created by p5
      for (const e of this._elements) {
        if (e.elt && e.elt.parentNode) {
          e.elt.parentNode.removeChild(e.elt);
        }
      }

      // Run `remove` hooks
      await this._runLifecycleHook('remove');
    }

    // remove window bound properties and methods
    if (this._isGlobal) {
      for (const p in p5.prototype) {
        try {
          delete window[p];
        } catch (x) {
          window[p] = undefined;
        }
      }
      for (const p2 in this) {
        if (this.hasOwnProperty(p2)) {
          try {
            delete window[p2];
          } catch (x) {
            window[p2] = undefined;
          }
        }
      }
      p5.instance = null;
    }
  }

  async _runLifecycleHook(hookName) {
    await Promise.all(p5.lifecycleHooks[hookName].map(hook => {
      return hook.call(this);
    }));
  }

  _initializeInstanceVariables() {
    this._accessibleOutputs = {
      text: false,
      grid: false,
      textLabel: false,
      gridLabel: false
    };

    this._styles = [];
    this._downKeys = {}; //Holds the key codes of currently pressed keys
    this._downKeyCodes = {};
  }
}

// Global helper function for binding properties to window in global mode
function createBindGlobal(instance) {
  return function bindGlobal(property) {
    if (property === 'constructor') return;

    // Check if this property has a getter on the instance or prototype
    const instanceDescriptor = Object.getOwnPropertyDescriptor(
      instance,
      property
    );
    const prototypeDescriptor = Object.getOwnPropertyDescriptor(
      p5.prototype,
      property
    );
    const hasGetter = (instanceDescriptor && instanceDescriptor.get) ||
                     (prototypeDescriptor && prototypeDescriptor.get);

    // Only check if it's a function if it doesn't have a getter
    // to avoid actually evaluating getters before things like the
    // renderer are fully constructed
    let isPrototypeFunction = false;
    let isConstant = false;
    let constantValue;

    if (!hasGetter) {
      const prototypeValue = p5.prototype[property];
      isPrototypeFunction = typeof prototypeValue === 'function';

      // Check if this is a true constant from the constants module
      if (!isPrototypeFunction && constants[property] !== undefined) {
        isConstant = true;
        constantValue = prototypeValue;
      }
    }

    if (isPrototypeFunction) {
      // For regular functions, cache the bound function
      const boundFunction = p5.prototype[property].bind(instance);
      Object.defineProperty(window, property, {
        configurable: true,
        enumerable: true,
        value: boundFunction
      });
    } else if (isConstant) {
      // For constants, cache the value directly
      Object.defineProperty(window, property, {
        configurable: true,
        enumerable: true,
        value: constantValue
      });
    } else if (hasGetter || !isPrototypeFunction) {
      // For properties with getters or non-function properties, use lazy optimization
      // On first access, determine the type and optimize subsequent accesses
      let lastFunction = null;
      let boundFunction = null;
      let isFunction = null; // null = unknown, true = function, false = not function

      Object.defineProperty(window, property, {
        configurable: true,
        enumerable: true,
        get: () => {
          const currentValue = instance[property];

          if (isFunction === null) {
            // First access - determine type and optimize
            isFunction = typeof currentValue === 'function';
            if (isFunction) {
              lastFunction = currentValue;
              boundFunction = currentValue.bind(instance);
              return boundFunction;
            } else {
              return currentValue;
            }
          } else if (isFunction) {
            // Optimized function path - only rebind if function changed
            if (currentValue !== lastFunction) {
              lastFunction = currentValue;
              boundFunction = currentValue.bind(instance);
            }
            return boundFunction;
          } else {
            // Optimized non-function path
            return currentValue;
          }
        }
      });
    }
  };
}

// Attach constants to p5 prototype
for (const k in constants) {
  p5.prototype[k] = constants[k];
}

p5.registerAddon(transform);
p5.registerAddon(structure);
p5.registerAddon(environment);
p5.registerAddon(rendering);
p5.registerAddon(renderer);
p5.registerAddon(renderer2D);
p5.registerAddon(graphics);

//////////////////////////////////////////////
// PUBLIC p5 PROPERTIES AND METHODS
//////////////////////////////////////////////

/**
 * A function that's called once when the sketch begins running.
 *
 * Declaring the function `setup()` sets a code block to run once
 * automatically when the sketch starts running. It's used to perform
 * setup tasks such as creating the canvas and initializing variables:
 *
 * ```js
 * function setup() {
 *   // Code to run once at the start of the sketch.
 * }
 * ```
 *
 * Code placed in `setup()` will run once before code placed in
 * <a href="#/p5/draw">draw()</a> begins looping.
 * If `setup()` is declared `async` (e.g. `async function setup()`),
 * execution pauses at each `await` until its promise resolves.
 * For example, `font = await loadFont(...)` waits for the font asset
 * to load because `loadFont()` function returns a promise, and the await
 * keyword means the program will wait for the promise to resolve.
 * This ensures that all assets are fully loaded before the sketch continues.
 *
 *
 * loading assets.
 *
 * Note: `setup()` doesn’t have to be declared, but it’s common practice to do so.
 *
 * @method setup
 * @for p5
 *
 * @example
 * function setup() {
 *   createCanvas(100, 100);
 *
 *   background(200);
 *
 *   // Draw the circle.
 *   circle(50, 50, 40);
 *
 *   describe('A white circle on a gray background.');
 * }
 *
 * @example
 * function setup() {
 *   createCanvas(100, 100);
 *
 *   // Paint the background once.
 *   background(200);
 *
 *   describe(
 *     'A white circle on a gray background. The circle follows the mouse as the user moves, leaving a trail.'
 *   );
 * }
 *
 * function draw() {
 *   // Draw circles repeatedly.
 *   circle(mouseX, mouseY, 40);
 * }
 *
 * @example
 * let img;
 *
 * async function setup() {
 *   img = await loadImage('assets/bricks.jpg');
 *
 *   createCanvas(100, 100);
 *
 *   // Draw the image.
 *   image(img, 0, 0);
 *
 *   describe(
 *     'A white circle on a brick wall. The circle follows the mouse as the user moves, leaving a trail.'
 *   );
 * }
 *
 * function draw() {
 *   // Style the circle.
 *   noStroke();
 *
 *   // Draw the circle.
 *   circle(mouseX, mouseY, 10);
 * }
 */
/**
 * A function that's called repeatedly while the sketch runs.
 *
 * Declaring the function `draw()` sets a code block to run repeatedly
 * once the sketch starts. It’s used to create animations and respond to
 * user inputs:
 *
 * ```js
 * function draw() {
 *   // Code to run repeatedly.
 * }
 * ```
 *
 * This is often called the "draw loop" because p5.js calls the code in
 * `draw()` in a loop behind the scenes. By default, `draw()` tries to run
 * 60 times per second. The actual rate depends on many factors. The
 * drawing rate, called the "frame rate", can be controlled by calling
 * <a href="#/p5/frameRate">frameRate()</a>. The number of times `draw()`
 * has run is stored in the system variable
 * <a href="#/p5/frameCount">frameCount()</a>.
 *
 * Code placed within `draw()` begins looping after
 * <a href="#/p5/setup">setup()</a> runs. `draw()` will run until the user
 * closes the sketch. `draw()` can be stopped by calling the
 * <a href="#/p5/noLoop">noLoop()</a> function. `draw()` can be resumed by
 * calling the <a href="#/p5/loop">loop()</a> function.
 *
 * @method draw
 * @for p5
 *
 * @example
 * function setup() {
 *   createCanvas(100, 100);
 *
 *   // Paint the background once.
 *   background(200);
 *
 *   describe(
 *     'A white circle on a gray background. The circle follows the mouse as the user moves, leaving a trail.'
 *   );
 * }
 *
 * function draw() {
 *   // Draw circles repeatedly.
 *   circle(mouseX, mouseY, 40);
 * }
 *
 * @example
 * function setup() {
 *   createCanvas(100, 100);
 *
 *   describe(
 *     'A white circle on a gray background. The circle follows the mouse as the user moves.'
 *   );
 * }
 *
 * function draw() {
 *   // Paint the background repeatedly.
 *   background(200);
 *
 *   // Draw circles repeatedly.
 *   circle(mouseX, mouseY, 40);
 * }
 *
 * @example
 * // Double-click the canvas to change the circle's color.
 *
 * function setup() {
 *   createCanvas(100, 100);
 *
 *   describe(
 *     'A white circle on a gray background. The circle follows the mouse as the user moves. The circle changes color to pink when the user double-clicks.'
 *   );
 * }
 *
 * function draw() {
 *   // Paint the background repeatedly.
 *   background(200);
 *
 *   // Draw circles repeatedly.
 *   circle(mouseX, mouseY, 40);
 * }
 *
 * // Change the fill color when the user double-clicks.
 * function doubleClicked() {
 *   fill('deeppink');
 * }
 */

/**
 * Turns off the parts of the Friendly Error System (FES) that impact performance.
 *
 * The <a href="https://github.com/processing/p5.js/blob/main/contributor_docs/friendly_error_system.md" target="_blank">FES</a>
 * can cause sketches to draw slowly because it does extra work behind the
 * scenes. For example, the FES checks the arguments passed to functions,
 * which takes time to process. Disabling the FES can significantly improve
 * performance by turning off these checks.
 *
 * @static
 * @property {Boolean} disableFriendlyErrors
 *
 * @example
 * // Disable the FES.
 * p5.disableFriendlyErrors = true;
 *
 * function setup() {
 *   createCanvas(100, 100);
 *
 *   background(200);
 *
 *   // The circle() function requires three arguments. The
 *   // next line would normally display a friendly error that
 *   // points this out. Instead, nothing happens and it fails
 *   // silently.
 *   circle(50, 50);
 *
 *   describe('A gray square.');
 * }
 */

/**
 * Loads a p5.js library.
 *
 * A library is a function that adds functionality to p5.js by adding methods
 * and properties for sketches to use, or for automatically running code at
 * different stages of the p5.js lifecycle. Take a look at the
 * <a href="/contribute/creating_libraries/">contributor docs for creating libraries</a>
 * to learn more about creating libraries.
 *
 * @static
 * @method registerAddon
 * @param {Function} library The library function to register
 *
 * @example
 * function myAddon(p5, fn, lifecycles) {
 *   fn.sayHello = function() {
 *     this.textAlign(this.CENTER, this.CENTER);
 *     this.text('Hello!', this.width / 2, this.height / 2);
 *   };
 * }
 * p5.registerAddon(myAddon);
 *
 * function setup() {
 *   createCanvas(100, 100);
 *
 *   background(200);
 *   sayHello(); // The sayHello method is now available!
 *
 *   describe('The text "Hello!"');
 * }
 */

export { Renderer2D as R, p5 as p, renderer2D as r };
