UNPKG

p5

Version:

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

1,746 lines (1,566 loc) 48.9 kB
import { P as P2D, a0 as WEBGL, u as BLEND, a2 as _DEFAULT_FILL, a3 as _DEFAULT_STROKE, j as ROUND, a4 as REMOVE, a5 as SUBTRACT, a6 as DARKEST, a7 as LIGHTEST, a8 as DIFFERENCE, a9 as MULTIPLY, aa as EXCLUSION, ab as SCREEN, ac as REPLACE, ad as OVERLAY, ae as HARD_LIGHT, af as SOFT_LIGHT, ag as DODGE, ah as BURN, ai as ADD, aj as PIE, ak as CHORD, f as TWO_PI, S as SQUARE, k as PROJECT, B as BEVEL, l as MITER, O as OPEN, v as constants, al as VERSION } from './constants-BRcElHU3.js'; import transform from './core/transform.js'; import structure from './core/structure.js'; import environment from './core/environment.js'; import { G as Graphics, y as rendering, z as graphics } from './rendering-CvUVN-Vb.js'; import { R as Renderer, I as Image, r as renderer } from './p5.Renderer-R23xoC7s.js'; import { Element } from './dom/p5.Element.js'; import { MediaElement } from './dom/p5.MediaElement.js'; import { b as RGBHDR } from './creating_reading-Cr8L2Jnm.js'; import FilterRenderer2D from './image/filterRenderer2D.js'; import './math/p5.Matrix.js'; import { PrimitiveToPath2DConverter } from './shape/custom_shapes.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); if(!this.filterRenderer){ this.filterRenderer = new FilterRenderer2D(this); } // Set and return p5.Element this.wrappedElt = new Element(this.elt, this._pInst); this.clipPath = null; } 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); //accessible Outputs 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()); //accessible Outputs 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()); //accessible Outputs 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) { this.clipPath.addPath(visitor.path); 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); // 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(); 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() { this.drawingContext.clip(this.clipPath); 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 Image) { this.drawingContext.save(); this.drawingContext.setTransform(1, 0, 0, 1, 0, 0); this.drawingContext.scale( this._pixelDensity, this._pixelDensity ); this.drawingContext.clearRect(x, y, imgOrCol.width, imgOrCol.height); this.drawingContext.drawImage(imgOrCol.canvas, x, y); this.drawingContext.restore(); } else { let r = 0, g = 0, b = 0, a = 0; let idx = 4 * (y * this._pixelDensity * (this.width * this._pixelDensity) + x * this._pixelDensity); if (!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.clipPa || this.drawingContext; const centerX = x + w / 2, centerY = y + h / 2, radiusX = w / 2, radiusY = h / 2; // 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) { if (!this._clipping) ctx.beginPath(); ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, start, stop); if (createPieSlice) ctx.lineTo(centerX, centerY); ctx.closePath(); if (!this._clipping) ctx.fill(); } // Stroke curves if (this.states.strokeColor) { if (!this._clipping) 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(); } if (!this._clipping) ctx.stroke(); } return this; } ellipse(args) { const ctx = this.clipPath || 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) ctx.beginPath(); ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); ctx.closePath(); if (!this._clipping && doFill) { ctx.fill(); } if (!this._clipping && doStroke) { ctx.stroke(); } } line(x1, y1, x2, y2) { const ctx = this.clipPath || this.drawingContext; if (!this.states.strokeColor) { return this; } else if (this._getStroke() === styleEmpty) { return this; } if (!this._clipping) ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); return this; } point(x, y) { const ctx = this.clipPath || 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) { // swapping fill color to stroke and back after for correct point rendering this._setFill(s); } if (!this._clipping) ctx.beginPath(); ctx.arc(x, y, ctx.lineWidth / 2, 0, TWO_PI, false); if (!this._clipping) { ctx.fill(); this._setFill(f); } } quad(x1, y1, x2, y2, x3, y3, x4, y4) { const ctx = this.clipPath || 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) ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineTo(x3, y3); ctx.lineTo(x4, y4); ctx.closePath(); if (!this._clipping && doFill) { ctx.fill(); } if (!this._clipping && 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.clipPath || 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) 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 (!this._clipping && this.states.fillColor) { ctx.fill(); } if (!this._clipping && this.states.strokeColor) { ctx.stroke(); } return this; } triangle(args) { const ctx = this.clipPath || 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) ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineTo(x3, y3); ctx.closePath(); if (!this._clipping && doFill) { ctx.fill(); } if (!this._clipping && doStroke) { ctx.stroke(); } } ////////////////////////////////////////////// // 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); } } 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 {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) { ////////////////////////////////////////////// // PRIVATE p5 PROPERTIES AND METHODS ////////////////////////////////////////////// this.hitCriticalError = false; this._setupDone = false; this._userNode = node; this._curElement = null; this._elements = []; this._glAttributes = null; this._requestAnimId = 0; this._isGlobal = false; this._loop = true; this._startListener = null; this._initializeInstanceVariables(); this._events = { // keep track of user-events for unregistering later pointerdown: null, pointerup: null, pointermove: null, dragend: null, dragover: null, click: null, dblclick: null, mouseover: null, mouseout: null, keydown: null, keyup: null, keypress: null, wheel: null, resize: null, blur: null }; 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 if (window.DeviceOrientationEvent) { this._events.deviceorientation = null; } if (window.DeviceMotionEvent && !window._isNodeWebkit) { this._events.devicemotion = null; } // ensure correct reporting of window dimensions this._updateWindowSize(); const bindGlobal = (property) => { Object.defineProperty(window, property, { configurable: true, enumerable: true, get: () => { if(typeof this[property] === 'function'){ return this[property].bind(this); }else { return this[property]; } }, set: (newValue) => { Object.defineProperty(window, property, { configurable: true, enumerable: true, value: newValue, writable: true }); if (!p5.disableFriendlyErrors) { console.log(`You just changed the value of "${property}", which was a p5 global value. This could cause problems later if you're not careful.`); } } }); }; // 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); } // Bind events to window (not using container div bc key events don't work) for (const e in this._events) { const f = this[`_on${e}`]; if (f) { const m = f.bind(this); window.addEventListener(e, m, { passive: false }); this._events[e] = m; } } 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 registerAddon(addon) { const lifecycles = {}; 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]); } } } 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 sketch starts this._millisStart = window.performance.now(); const context = this._isGlobal ? window : this; if (typeof context.setup === 'function') { await context.setup(); } if (this.hitCriticalError) return; // unhide any hidden canvases that were created const canvases = document.getElementsByTagName('canvas'); // Apply touchAction = 'none' to canvases if pointer events exist if (Object.keys(this._events).some(event => event.startsWith('pointer'))) { for (const k of canvases) { k.style.touchAction = 'none'; } } for (const k of canvases) { 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'); } // 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 * <div> * <code> * // 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(); * } * </code> * </div> */ 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); } // unregister events sketch-wide for (const ev in this._events) { window.removeEventListener(ev, this._events[ev]); } // remove DOM elements created by p5, and listeners for (const e of this._elements) { if (e.elt && e.elt.parentNode) { e.elt.parentNode.removeChild(e.elt); } for (const elt_ev in e._events) { e.elt.removeEventListener(elt_ev, e._events[elt_ev]); } } // 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) { for(const hook of p5.lifecycleHooks[hookName]){ await 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 = {}; } } // Attach constants to p5 prototype for (const k in constants) { p5.prototype[k] = constants[k]; } ////////////////////////////////////////////// // 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 * <div> * <code> * function setup() { * createCanvas(100, 100); * * background(200); * * // Draw the circle. * circle(50, 50, 40); * * describe('A white circle on a gray background.'); * } * </code> * </div> * * <div> * <code> * 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); * } * </code> * </div> * * <div> * <code> * 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); * } * </code> * </div> */ /** * 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 * <div> * <code> * 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); * } * </code> * </div> * * <div> * <code> * 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); * } * </code> * </div> * * <div> * <code> * // 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'); * } * </code> * </div> */ /** * 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. * * @property {Boolean} disableFriendlyErrors * * @example * <div> * <code> * // 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.'); * } * </code> * </div> */ p5.disableFriendlyErrors = false; p5.registerAddon(transform); p5.registerAddon(structure); p5.registerAddon(environment); p5.registerAddon(rendering); p5.registerAddon(renderer); p5.registerAddon(renderer2D); p5.registerAddon(graphics); export { Renderer2D as R, p5 as p, renderer2D as r };