p5
Version:
[](https://www.npmjs.com/package/p5)
1,746 lines (1,566 loc) • 48.9 kB
JavaScript
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 };